前回の続きになります。先にこちらも読んで頂ければと。
WPF で Zune のようなウィンドウを作る
というか、前回「もうすぐ公開します」的な雰囲気出しておきながら 1 ヶ月放置というアレっぷり。。
はい、気を取り直して。
前回の投稿で Zune ライクなウィンドウを作りましたが、今回は Visual Studio 2012 です。
Zune、MetroTwit、GitHub for Windows と Visual Studio 2012 の違いといえば、Visual Studio の方は端が光ってるんですよね。しかも状況に応じて色が変わる。左から、起動時、編集中、デバッグ中、非アクティブ時です。
かっこいいですね。ちなみに私は濃色テーマが好きです。
まず思いつく方法
真っ先に思いついたのは、WindowStyle=”None” と AllowsTransparency=”True” にする方法。ウィンドウのクライアント領域と実際にコンテンツを配置する領域 (いわゆる LayoutRoot 的な) にマージンを設けて、DropShadowEffect で影をつけるというもの。
コードはこちら。Window の Background が Transparent になっていることと、8 行目の Border の Margin=”10″ がミソです。実際のウィンドウの大きさは、見た目より 10 px 大きいのです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Width="500" Height="200" AllowsTransparency="True" WindowStyle="None" Background="Transparent"> BorderThickness="1" Margin="10"> Color="#FF007ACC" /> |
これでいいじゃない、という場合はこれで完成です。
ですが、個人的にはこれではダメ… 前回の投稿の最後にも書きましたが、AllowsTransparency=”True” にすると挙動が通常のウィンドウと異なるのでよろしくないですし、今回のケースではウィンドウの実際のサイズと見た目のサイズが異なるのも微妙な感じ。。。
Visual Studio 2012 はどうなってるの?
じゃあ本家 Visual Studio 様はどういう実装になっているんだろう? と思って Spy++ で見てみたところ、こんな感じの造りになっていることが明らかに。
どうやら、色がついている端の部分の 1 px と合わせて 幅 9 px の VisualStudioGlowWindow という透明なウィンドウが張り付いているらしい。
ちなみに、Office 2013 も同じです。MSO_BORDEREFFECT_WINDOW_CLASS という透明なウィンドウがいて、影をつけています。
余談ですが、同僚とこの話をしたところ「どうして Visual Studio 開発チームはそんなややこしいことまでして光らせたかったの」と。はい、私にもわかりません。でもかっこいいんだから仕方ない。
で、無謀ながらこれを目指して作ってみました。
先にネタばらししてしまうと、一応それっぽいものが出来上がります。ですが、記事 1 回で全てを解説しきれるコードの量ではなくなってしまったので、今回は要点のみ解説していきます。
完成したサンプル コードはこちら。
https://github.com/Grabacr07/MetroRadiance
GlowWindow を作る
何はともあれ、GlowWindow を作らないことには始まりません。まずは要件を整理。
- 幅 1 px の線と、光る影っぽい何か
- タスク バーに表示されない + アクティブ ウィンドウにならない
- アプリケーションのメイン ウィンドウの四辺に張り付き、移動とサイズ変更に追従する
- アプリケーションのメインウィンドウのサイズを変更させる
それぞれ順に見ていきましょう。なお、GlowWindow が張り付く先となるアプリケーションのメイン ウィンドウは、厳密には親子関係はありませんがここでは owner と呼んでいます。
幅 1 px の線と、光る影っぽい何か
これは簡単、GlowWindow の Content に Border を配置するだけです。
サンプルでは縦横どちらにも変えられる Glow というカスタム コントロールを作っていますが、中身は Border です。DropShadowEffect の Color は、Border の Background よりも明るめにした方が目立ってキレイかも。
1 2 3 4 5 6 7 |
ShadowDepth="0" BlurRadius="8" /> |
タスク バーに表示されない + アクティブ ウィンドウにならない
「タスク バーに表示されない」ですが、Window の ShowInTaskbar=”False” です。おわり。…と思っていた時期が私にもありました。いや、間違いではないのですが。実は「アクティブ ウィンドウにならない」の実装がかなり曲者で、WPF だけでは無理と判断し、仕方なく P/Invoke することに。で、結局 GlowWindow の拡張ウィンドウ スタイルから WS_EX_APPWINDOW を外し、WS_EX_NOACTIVE を指定するので、結果的にはわざわざ ShowInTaskbar=”False” する必要はなかったのです。
というわけで XAML から。こういうウィンドウでこそ WindowStyle=”None” するべきですよね、ということで、AllowsTransparency=”True” Background=”Transparent” と指定して透明なウィンドウにしています。
1 2 3 4 5 6 7 8 9 10 11 |
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:metro="clr-namespace:VS2012LikeWindow2.Views.MetroChrome" Title="GlowWindow" Width="300" Height="300" WindowStyle="None" AllowsTransparency="True" Background="Transparent" ShowActivated="False" ResizeMode="NoResize" SnapsToDevicePixels="True"> x:FieldModifier="private"/> |
次にコード ビハインド、の前に。
拡張ウィンドウ スタイルの取得と設定をするために、以下の関数を使用しています。
GetWindowLong function
SetWindowLong function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static class NativeMethods { [DllImport("user32.dll", EntryPoint = "GetWindowLongA", SetLastError = true)] public static extern int GetWindowLong(IntPtr hWnd, int nIndex); public static WSEX GetWindowLongEx(this IntPtr hWnd) { return (WSEX)NativeMethods.GetWindowLong(hWnd, (int)GWL.EXSTYLE); } [DllImport("user32.dll")] public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); public static WSEX SetWindowLongEx(this IntPtr hWnd, WSEX dwNewLong) { return (WSEX)NativeMethods.SetWindowLong(hWnd, (int)GWL.EXSTYLE, (int)dwNewLong); } /* 以下省略 */ } |
上記の関数を使用し、GlowWindow の拡張ウィンドウ スタイルを変えているのが以下のコード。
Window.SourceInitialized イベントは、Win32 との相互運用のためのイベントです。Initialized イベントの段階ではウィンドウ ハンドルを取得できませんが、SourceInitialized イベントの段階でならできます。まさに今回のような用途向け。
拡張ウィンドウ スタイルから WS_EX_APPWINDOW を外し、WS_EX_NOACTIVATE を追加しています。これで、タスク バーに表示されないウィンドウになります。
1 2 3 4 5 6 7 8 9 10 11 |
protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); var source = (HwndSource)HwndSource.FromVisual(this); var wsex = source.Handle.GetWindowLongEx(); wsex ^= WSEX.APPWINDOW; wsex |= WSEX.NOACTIVATE; source.Handle.SetWindowLongEx(wsex); source.AddHook(this.WndProc); } |
上記ソースの最後、HwndSource.AddHook メソッドの引数で指定されていた、WndProc メソッドの中身です。いわゆるウィンドウ プロシージャのようなもの。
非アクティブウィンドウ上でユーザーがマウス ボタンを押すと、WM_MOUSEACTIVATE メッセージが送られます。これを処理してあげないと、ウィンドウがアクティブになってしまいます。
WM_MOUSEACTIVATE message
1 2 3 4 5 6 7 8 9 |
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == (int)WM.MOUSEACTIVATE) { handled = true; return new IntPtr(3); } return IntPtr.Zero; } |
これで、タスク バーに表示されず、ユーザーがクリックしてもアクティブにならないウィンドウができました。
アプリケーションのメイン ウィンドウの四辺に張り付き、移動とサイズ変更に追従する
レッツ座標計算。
サンプルでは、owner の上下左右それぞれで owner の位置とサイズを使用し GlowWindow の位置とサイズを計算する Func を作っています。計算式は割愛。
そして、計算した位置とサイズは、SetWindowPos 関数で適用します。
SetWindowPos function
1 2 3 4 5 6 7 8 |
static class NativeMethods { [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, SWP flags); /* 以下省略 */ } |
この関数を使い、計算した位置とサイズ (getLeft() や this.getWidth() などの Func
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// コンストラクター owner.LocationChanged += (sender, e) => this.Update(); owner.SizeChanged += (sender, e) => this.Update(); owner.StateChanged += (sender, e) => this.Update(); } private void Update() { NativeMethods.SetWindowPos( this.handle, this.ownerHandle, (int)this.getLeft(), (int)this.getTop(), (int)this.getWidth(), (int)this.getHeight(), SWP.NOACTIVATE); } // サンプルはもう少し処理が増えてますが、重要なのは上記のコードです。 |
アプリケーションのメインウィンドウのサイズを変更させる
これが一番厄介でした… まず、Visual Studio 2012 の動きから。
ご覧のように、Visual Studio のウィンドウの外にサイズ変更カーソルが出ています。そう、Visual Studio 本体でなく GlowWindow 上に出てるんですよね、これ。
なので、owner のサイズ変更幅はゼロ (前回の WindowChrome クラスで言うところの WindowChrome.ResizeBorderThickness=”0″) です。GlowWindow 上でのドラッグ操作で owner のサイズ変更をさせる、という実装にする必要があります。
いろいろ考えましたが、最終的に以下の実装になりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == (int)WM.LBUTTONDOWN) { var pt = new Point((int)lParam & 0xFFFF, ((int)lParam >> 16) & 0xFFFF); NativeMethods.PostMessage(this.ownerHandle, (uint)WM.NCLBUTTONDOWN, (IntPtr)this.getHitTestValue(pt), IntPtr.Zero); } if (msg == (int)WM.NCHITTEST) { var ptScreen = new Point((int)lParam & 0xFFFF, ((int)lParam >> 16) & 0xFFFF); var ptClient = this.PointFromScreen(ptScreen); var cursor = this.getCursor(ptClient); if (cursor != this.Cursor) this.Cursor = cursor; } return IntPtr.Zero; } |
まず、GlowWindow 上での LBUTTONDOWN メッセージ (左クリック)。
このメッセージを受けたら、PostMessage 関数で owner に NCLBUTTONDOWN メッセージ (非クライアント領域上の左クリック) を投げます (6 行目)。すると、owner は非クライアント領域 (ウィンドウの端) で左クリックされたと思い込み、ウィンドウのリサイズを開始してくれます。
PostMessage の 3 つめの引数では、ウィンドウのどの部分でクリックされたかを指定しています (ウィンドウ左端なら HTLEFT、左上端なら HTTOPLEFT、右下端なら HTBOTTOMRIGHT など。getHitTestValue はそれを算出する Func<Point, HitTestValues> です)。
8 行目以降の NCHITTEST メッセージに対する処理は、マウスカーソルを設定するためのものです。
マウス ポインターが GlowWindow のどの位置にいるかによって、どの方向のリサイズ カーソルを表示するのかを決定し、設定しています。
最終的にはこんな感じに。ちゃんとドラッグすればウィンドウ本体がリサイズされます。
で、最終的に
いくつかポイントを解説しましたが、完成したのが次のような感じ。
ボタンでアクセント カラーを変えられるようにしてみました。
まとめ
というわけで、Visual Studio 2012 のウィンドウかっこいいなー作りたいなーから始まったネタでしたが、蓋を開けてみれば Win32 相互運用だらけで誰も得をしないような謎記事になってしまいました。これはひどい。というか記事長すぎですね。反省。
完成したサンプル コードはこちら。”3. VS2012LikeWindow2″ が今回のプロジェクトです。それっぽい外観になっていると思います (.NET Framework 4.5 が必要です)。
https://github.com/Grabacr07/MetroRadiance
今回はあくまで例、似せてみたよ~というだけですので、本物の VisualStudioGlowWindow の動作には程遠い感じ。まともな Windows アプリケーションは .NET Framework でしか組んだことがないので、Win32 の知識不足も相まってこの有様です。もし「ここが違う」ですとか「もっとうまい方法がある」といった何かがありましたら、Twitter などでこっそり教えて頂けたら、それはとっても嬉しいなって。
Pingback: Acer のスレート PC 「ICONIA W700」 買ってみた! | grabacr.net
Nice article!
I Got your source code from github, but I can’t compile it in VS2012 Professional without Livet.dll and Microsoft.Expression.Interactions dll.
Can you tell me how to solve it?
Thank you.
Thank you for your comments!
Please right click on the Solution node in Solution Explorer and select [Enable NuGet Package Restore], and try to build again.
http://docs.nuget.org/docs/workflows/using-nuget-without-committing-packages
Thank you for your reply. It worked.
When I click icon in taskbar to minimize the window, and click again to restore it that
the glow window not visible.
I check the code for a long while, andI hook the WM_SYSCOMMAND (SC_RESTORE) message in MetroWindow and call UpdateGlowWindows(), also not work.
Can you give me some tip to fix this problem? Thank you.
Pingback: ItemsControl 攻略 ~ 外観のカスタマイズ | grabacr.nét
VS2012のスタイルを探しているときにこの記事に出会いました。
WPFの経験値が皆無なので、必死になってこのコードを追っています。
とりあえず、WPFアプリをDLL化し、WindowsFormHostで既存のWinFormコントロールをはめ込めるようにして既存資産の活用に成功しました。
が、枠の光る部分が何故か動かなくなってしまいました。
デバッガには
System.Windows.ResourceDictionary Warning: 9 : Resource not found; ResourceKey=’AccentBrushKey’
System.Windows.ResourceDictionary Warning: 9 : Resource not found; ResourceKey=’ItemMouseOverBackgroundBrushKey’
System.Windows.ResourceDictionary Warning: 9 : Resource not found; ResourceKey=’ItemMouseOverBorderBrushKey’
みたいなエラーが出ているのでリソースの取得に失敗している?というところまでは推測できました。
これだけの状況で判断できないとは思いますが、何か思い当たるような現象は無いでしょうか?
あと寄付とかってできませんか?
個人的にこの記事は非常に価値があると思いますし、有償ライブラリ化しても需要はあると思います。
コメントありがとうございます。
この記事で作成した Visual Studio 2012 風の光るウィンドウですが、現在、その発展形となる実装を KanColleViewer という Windows Desktop アプリケーションで使用しております。
https://github.com/Grabacr07/KanColleViewer
https://twitter.com/Grabacr07/status/399497651776806912
光るウィンドウの実装は Grabacr07.Desktop.Metro というプロジェクトにまとめて DLL 化しており、それを Grabacr07.KanColleViewer プロジェクトから参照して使用しています。
ご指摘の現象は、おっしゃる通り DLL 化により DynamicResource によるリソースの解決に失敗してしまい発生していると考えられます。Grabacr07.Desktop.Metro ライブラリでは、光る部分の Brush を外部から設定できるようにしてありますので、GlowMetroWindow クラスの BorderBrush および InactiveBorderBrush を設定するだけで動作するはずです。
よろしければ、本記事の MetroLikeWindow だけでなく、KanColleViewer のソースも参考にして頂ければと思います。
寄付については… 私もいろいろ試行錯誤しつつ勉強している身ですので、考えておりませんでした。。。
光るウィンドウもまだまだ発展途上ですし、お気持ちだけ頂きます。ありがとうございます。
Pingback: Visual Studio 2012 のような光るウィンドウを作る (再)、そして WPF での高 DPI 対応 | grabacr.nét