WPF:子ウィンドウを透明にするには?[C#/VB].NET TIPS

WPFアプリでは、.NET Framework 4.6でサポートされた新機能を使うことで、子ウィンドウを透明にできる。本稿ではその方法を解説する。

» 2015年10月21日 05時00分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Development]
.NET TIPS
Insider.NET

 

「.NET TIPS」のインデックス

連載目次

対象:.NET 4.6以降/Windows 8.0以降


 「子ウィンドウ」といっても、WPFのウィンドウ(System.Windows名前空間のWindowクラス)のことではない。Win32 APIの、すなわちWindowsが直接管理している生粋のウィンドウの話である。以降では「ウィンドウ(WPF)」「ウィンドウ(Win32)」と表記して区別する。

 WPFでウィンドウ(WPF)を透明/半透明にするのは簡単だ(「.NET TIPS:WPF:ウィンドウを透明にするには?[C#/VB]」参照)。しかし、WPFで作った子ウィンドウ(Win32)は、これまで透明にできなかったのである。それが、Windows 8.0以降と.NET Framework 4.6以降の組み合わせで可能になった。本稿ではその方法を説明する。

子ウィンドウ(Win32)が必要な場面

 いろいろな場面が想定できるが、Win32と相互運用する場面でWPFのUIコントロールを最前面に表示しようとすると子ウィンドウ(Win32)が必須になる。例えば、WebBrowserコントロール(System.Windows.Controls名前空間)の手前にWPFのUIコントロールを配置したいような場合だ。そのような場合、MSDNの「WPF と Win32 の相互運用性」には「HwndHostは、同じトップレベルウィンドウの他のWPF要素の上に表示」されると記述されている。その意味するところは、WebBrowserコントロール(これはHwndHostの一つ)が必ず最前面に表示されるので、その他のWPFのUIコントロールはWebBrowserコントロールの手前に表示できないということである。

 なお、HwndHostには、WindowsフォームのUIコントロールをWPFで利用するためのWindowsFormsHostコントロール(System.Windows.Forms.Integration名前空間)も含まれる。以前からWPFに取り組んできた開発者には、この問題に遭遇した人も多いのではないだろうか。

 実際に問題を確認してみよう。WPFのプロジェクトを作り、次のようなXAMLコードを記述する。

<Window x:Class=……省略……
        >
  <Grid x:Name="rootGrid" Background="#e0e0e0">
    <WebBrowser Margin="64,32" Panel.ZIndex="1" 
                Source="……省略(WebページのURL)……" />
    <Border Background="DarkGreen" Margin="100,60" Panel.ZIndex="10" />
    <TextBlock x:Name="textBlock1" Margin="4" HorizontalAlignment="Right" VerticalAlignment="Bottom"
               >Imageコントロール</TextBlock>
    <Grid x:Name="footPrint" HorizontalAlignment="Right" VerticalAlignment="Bottom"
          Panel.ZIndex="20" Background="#4400a2e8">
      <Border BorderBrush="Blue" BorderThickness="4" Opacity="0.25" />
      <Image Source="……省略(画像ファイルの相対パス)……" Width="208" Height="208" Opacity="0.75"/>
    </Grid>
    <Button x:Name="button1" Click="Button_Click" Content="子ウィンドウ化"
            HorizontalAlignment="Center" VerticalAlignment="Bottom"
            Margin="4" Padding="8,0"
            />
  </Grid>
</Window>

WebBrowserコントロールの手前に他のコントロールを配置する(XAML)
WebBrowserコントロールの手前にBorderコントロールとGridコントロール(名前「footPrint」)を配置した。XAMLコードの記述順に奥から手前へと配置されるのだが、さらにZIndexプロパティも指定してある(数字の大きい方が手前に表示される)。
ウィンドウ(WPF)のトップレベルのGridコントロールには「rootGrid」という名前が付けてある。また、ButtonコントロールのClickイベントハンドラー「Button_Click」をコードビハインドに生成しておいてほしい(メソッド内は空のままでよい)。
なお、後に説明するコードビハインドでは、「rootGrid」の子要素になっている「footPrint」を、その親から切り離して子ウィンドウ(Win32)に移動させる。

 上のコードを記述しているVisual Studio 2015のXAMLエディターは、次の画像のようになる。

上記のコードをVisual Studioで記述したところ(XAMLエディター) 上記のコードをVisual Studioで記述したところ(XAMLエディター)
WebBrowserコントロールは濃いめの灰色の部分だ。その内側の手前に緑色のBorderコントロールが配置されている。さらに手前にGridコントロール「footPrint」がある(青色の四角形と茶色の足跡の画像)。
このようにXAMLエディター上では、XAMLコードに記述した順序で表示されている。
なお、本稿で使っているのは無償のVisual Studio Community 2015である。

 ところが実行してみると、次の画像のようにWebBrowserコントロールが最も手前に表示されてしまうのだ。

上記のコードを実行したところ(Windows 10) 上記のコードを実行したところ(Windows 10)
WebBrowserコントロールには、Sourceプロパティに指定したWebページが表示されている。
WebBrowserコントロールの手前に配置されるはずの緑色のBorderコントロールは完全に隠されてしまった。Gridコントロール「footPrint」(青色の四角形と茶色の足跡の画像)も、WebBrowserコントロールの下になっている。コントロールに指定したZIndexプロパティも無視されてしまった。

 これは、WebBrowserコントロールがHwndHostであり、ウィンドウ(WPF)とは独立した子ウィンドウ(Win32)になっているためだ。親ウィンドウ(WPF)の一部であるWPFのUIコントロールは、子ウィンドウ(Win32)の手前にくることはできないのである。

 これを解決するには、子ウィンドウ(Win32)に勝てるのは子ウィンドウ(Win32)だけであるから、WPFのプログラム中で子ウィンドウ(Win32)を作成し、そこにWPFのコントロールを配置すればよい。それは従来から可能であった。ただ、子ウィンドウ(Win32)を透明にする手段は提供されていなかったのである。

子ウィンドウ(Win32)を透明にするには?

 正確には、「WPFのコントロールを配置できる子ウィンドウ(Win32)を透明にするには?」と言うべきであろう。それは、今まではサポートされていなかったのである。

 それが、Windows 8.0以降と.NET Framework 4.6以降の組み合わせで可能になった。Windows 8.0より以前のバージョンでは動作しないので、ご注意願いたい。なお、以降の説明ではVisual Studio 2015を使用する*1

*1 Visual Studio 2012/2013でも、.NET Framework 4.6を導入し.NET Framework 4.6 Targeting Packを利用すれば開発できるはずである(筆者は確認していない)。


 まず、プロジェクトの対象フレームワークを.NET Framework 4.6に変更する。既存のプロジェクトの場合は、プロジェクトのプロパティの[アプリケーション]タブで[対象のフレームワーク]を切り替える。新規にプロジェクトを作る場合は、プロジェクトを指定するダイアログの上部で.NET Frameworkのバージョンを設定する(次の画像)。

新規にプロジェクトを作るダイアログで.NET Frameworkのバージョンを指定する 新規にプロジェクトを作るダイアログで.NET Frameworkのバージョンを指定する
ダイアログ上部のドロップダウンで切り替える(赤枠内)。

 次に、マニフェストファイルを追加し、「このプログラムが動作するのはWindows 8.0以降である」と宣言する。

 マニフェストファイルを追加するには、プロジェクトに新しい項目を追加するダイアログで[全般]カテゴリにある[アプリケーション マニフェスト ファイル]を指定し、ファイル名は「app.manifest」のまま、[追加]ボタンをクリックする(次の画像)。

プロジェクトにマニフェストファイルを追加する プロジェクトにマニフェストファイルを追加する
[アプリケーション マニフェスト ファイル]を選択し、ファイル名は「app.manifest」のままにして、[追加]ボタンをクリックする。

 追加したマニフェストファイルを開くと、ファイルの中ほどにWindows 8に関する<supportedOS>タグがコメントアウトされているので、そのコメントを外す(次の画像)。

マニフェストファイルでWindows 8以降であるという宣言をする マニフェストファイルでWindows 8以降であるという宣言をする
マニフェストファイルの中ほどにWindows 8に関する<supportedOS>タグがある(赤枠内)。この行のコメントを外す。

 もしも<supportedOS>タグを含むセクションが存在しない場合は、次のコードのようにマニフェストファイルの末尾に追記する。

  <!-- 追加部分:ここから -->
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
    <application>
      <!-- Windows 8 -->
      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
    </application>
  </compatibility>
  <!-- 追加部分:ここまで -->
</assembly>

マニフェストファイルに追加するコード(XML)
マニフェストファイルに<supportedOS>タグを含むセクションが存在しない場合は、ファイル末尾の</assembly>閉じタグのすぐ上にこのように追記する。

 最後にコードビハインドを編集する。ここではボタンのクリックイベントハンドラーから呼び出すようにした。透明な子ウィンドウ(Win32)を作り、そこにWPFのコントロールを配置するのだ(次のコード)。

private void Button_Click(object sender, RoutedEventArgs e)
{
  button1.IsEnabled = false;
  ShowChildWindow();
}

private void ShowChildWindow()
{
  // このウィンドウ(WPF)のウィンドウ(Win32)ハンドルを取得する
  IntPtr parentWindowHandle 
    = new System.Windows.Interop.WindowInteropHelper(this).Handle;

  // 透明な子ウィンドウ(Win32)を作る
  const int WS_CHLID = 0x40000000;
  const int WS_CLIPCHILDREN = 0x02000000;
  const int WS_VISIBLE = 0x10000000;
  var windowParams 
    = new System.Windows.Interop.HwndSourceParameters(
                                  "dotNetTipsTransparentChildWindow");
  windowParams.ParentWindow = parentWindowHandle;
  windowParams.WindowStyle = WS_CHLID | WS_CLIPCHILDREN | WS_VISIBLE;
  windowParams.UsesPerPixelTransparency = true; // .NET 4.6で新設
  windowParams.PositionX = 0;
  windowParams.PositionY = 0;
  windowParams.Width = (int)rootGrid.ActualWidth;
  windowParams.Height = (int)rootGrid.ActualHeight;
  var hwndsSrc = new System.Windows.Interop.HwndSource(windowParams);
  // ↑マニフェストにWindows 8以降の宣言がないと、
  //   この行でSystem.ComponentModel.Win32Exceptionが発生する

  // rootGridからUIコントロールを切り離し、子ウィンドウ(Win32)に移す
  rootGrid.Children.Remove(footPrint);
  hwndsSrc.RootVisual = footPrint;

  // 動作確認用
  textBlock1.Text = "子ウィンドウ化";
}

Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
  button1.IsEnabled = False
  ShowChildWindow()
End Sub

Private Sub ShowChildWindow()
  ' このウィンドウ(WPF)のウィンドウ(Win32)ハンドルを取得する
  Dim parentWindowHandle As IntPtr _
    = New System.Windows.Interop.WindowInteropHelper(Me).Handle

  ' 透明な子ウィンドウ(Win32)を作る
  Const WS_CHLID As Integer = &H40000000
  Const WS_CLIPCHILDREN As Integer = &H02000000
  Const WS_VISIBLE As Integer = &H10000000
  Dim windowParams _
    = New System.Windows.Interop.HwndSourceParameters(
                                  "dotNetTipsTransparentChildWindow")
  windowParams.ParentWindow = parentWindowHandle
  windowParams.WindowStyle = WS_CHLID Or WS_CLIPCHILDREN Or WS_VISIBLE
  windowParams.UsesPerPixelTransparency = True ' .NET 4.6で新設
  windowParams.PositionX = 0
  windowParams.PositionY = 0
  windowParams.Width = rootGrid.ActualWidth
  windowParams.Height = rootGrid.ActualHeight
  Dim hwndsSrc = New System.Windows.Interop.HwndSource(windowParams)
  ' ↑マニフェストにWindows 8以降の宣言がないと、
  '   この行でSystem.ComponentModel.Win32Exceptionが発生する

  ' rootGridからUIコントロールを切り離し、子ウィンドウ(Win32)に移す
  rootGrid.Children.Remove(footPrint)
  hwndsSrc.RootVisual = footPrint

  ' 動作確認用
  textBlock1.Text = "子ウィンドウ化"
End Sub

透明な子ウィンドウ(Win32)を作りWPFのコントロールを配置するコード(上:C#、下:VB)
コードビハインドで従来と異なるところは、.NET Framework 4.6でHwndSourceParametersオブジェクト(System.Windows.Interop名前空間)に追加されたUsesPerPixelTransparencyプロパティ(太字の部分)にtrueをセットしている部分だけである。それだけで、作成された子ウィンドウ(Win32)は透明になる。
このコードに登場するUIコントロールについては、前掲したXAMLコードをご覧いただきたい。
なお、このコードでは、XAMLコードで定義したUIコントロールを子ウィンドウ(Win32)に移すという手段を採ったが、C#/VBのコードで直に生成したUIコントロールを子ウィンドウ(Win32)にセットしても構わない。

 これで実行してみると次の画像のようになる。WebBrowserコントロールの手前に、半透明のコントロールが表示できた。

上記のコードを実行したところ(Windows 10) 上記のコードを実行したところ(Windows 10)
最初の画像ではWebBrowserコントロールに隠されていたGridコントロール「footPrint」(青色の四角形と茶色の足跡の画像)が、今度はちゃんと手前に表示されている。
なお、子ウィンドウ(Win32)は親ウィンドウ(WPF)とは独立した存在なので、親ウィンドウ(WPF)をリサイズしても子ウィンドウ(Win32)のサイズは変わらない。今回の例で言うと、親ウィンドウ(WPF)をリサイズしても「footPrint」(青色の四角形と茶色の足跡の画像)の位置は変わらないのである。

まとめ

 WPFで子ウィンドウ(Win32)を透明にするには、.NET Framework 4.6でHwndSourceParametersオブジェクトに追加されたUsesPerPixelTransparencyプロパティをtrueに設定すればよい。ただし、マニフェストファイルに「このプログラムが動作するのはWindows 8.0以降である」という宣言が必要である。

利用可能バージョン:.NET Framework 4.6以降
カテゴリ:WPF/XAML 処理対象:Windowコントロール
カテゴリ:WPF/XAML 処理対象:WebBrowserコントロール
カテゴリ:WPF/XAML 処理対象:WindowsFormsHostコントロール
使用ライブラリ:HwndSourceクラス(System.Windows.Interop名前空間)
関連TIPS:WPF:ウィンドウを透明にするには?[C#/VB]
関連TIPS:WPFアプリケーションでWebページを表示するには?[3.5 SP1、C#、VB]
関連TIPS:WPF/XAMLでWindowsフォームを利用するには?[3.0、3.5、VS 2008、C#、VB]


「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。