ウィンドウの位置を変更する方法 (Matt Gertz)

"パジャマプログラマ" をサポートする

在宅勤務はすばらしいことです。環境にやさしい人でありたい私としては、現実的に実行できる場合は心から支持したいと考えます。私の現在の仕事は、人と対面するミーティングが多く、実際は在宅勤務する機会があまりありません。しかし、投稿するブログを練るときなどに夕方家で働くので、家から仕事ができる環境をセットアップすることは重要です。

ここで問題となるのは、会社のコンピュータはデュアルモニタであり、家のコンピュータはシングルモニタなことです。そのため、家に帰る前にすべてのウィンドウを第 1 モニタに移動させる決意をしない限り、家から会社のコンピュータにリモートでアクセスすると、必要なウィンドウの多くが画面の外に出てしまうことになります。ウィンドウを "並べて表示" したり "重ねて表示" したりするコマンドは、ウィンドウを現在のモニタから移動するわけではないので、効果がありません。もちろん、該当するウィンドウのアイコンをタスクバーで右クリックして [移動] をクリックし、カーソルキーを押してウィンドウをゆっくりと第 1 モニタに動かすことができますが、実用的な方法ではありません。私が望むのは、「ここへ来い!」の一言で 1 つ以上のウィンドウを手早く移動する方法なのです。

現在この手の操作を行うフリーウェアが多数あることを知っていますが、WPF (Windows Presentation Foundation、以前の "Avalon") ウィンドウで遊ぶチャンスを探してもいたので、今まで使う機会が少なかった WPF を学ぶ手段として、自作のコードで WPF ウィンドウを統合する時間を取ることにしました。この投稿では、いかにも WPF 的な実演をお見せするつもりはなく、透明、立体表示なども扱いません。この演習は、トリッキーなことに取り組む前に、デザイナやプロパティのバリエーションに慣れるのが主な目的です。

自作コードの目標は、システム上でタイトルが付いたウィンドウをすべて一覧表示し、選択したウィンドウを確実に第 1 モニタ上にある (10,10) の位置に移動するボタンがあるアプリケーションを作成することです (VS2005 以前のバージョンのユーザー向け : ここでは、VS2008 で WPF ウィンドウを使用しますが、このコードがフォームで実行できない理由は少しもなく、きわめて単純に変換できます)。

デザイナで基本アプリケーションを作成する

まず、[新しいプロジェクト] をクリックし、[WPF アプリケーション] を Windows プロジェクトの種類の一覧から選択し、名前を付けて (この例では "VBGitOverHere") [OK] をクリックします。プロジェクトが作成されたら、以前のバージョンでフォーム用に使用していたデザイナとはやや異なる外観のデザイナが表示されます。最上部の外観はフォーム デザイナに似ていますが、最下部に XAML ウィンドウが表示されます。XAML は WPF のバックボーンであり、最終的にこの言語でウィンドウのレイアウトが記述されます。フォームのデザイナを使用したことがあれば、中では ActiveX フォームと類似しています (この例では XAML エディタを直接操作しませんが、この記事で取り上げる内容はすべてデザイナおよびエディタでも実行できます)。

ウィンドウのプロパティグリッドの操作方法は、フォームの場合とほぼ同じです。大きな違いの 1 つは、グリッド自体の中でなく、グリッドを含むプロパティウィンドウの最上部にオブジェクト名を指定することです。ここに移動して、好きな名前に変更します。また、[タイトル] プロパティを "VBGitOverHere" (または自分で識別できる他の名前) に変更します。Topmost を "True" に、WindowStartupLocation を "Manual" に設定し、これらの Left と Top を "10" に設定します。これで、このウィンドウは常に第 1 モニタの左上隅で他のウィンドウの上に開くことになります。こうすると、このアプリケーションが第 2 モニタに表示されることは絶対になくなり、ウィンドウがあちこちに移動する間も、このアプリケーションのウィンドウが常に最上部に表示されるようになります。

1 点注意すべきなのは、コントロールのクライアント領域とも考えられるグリッドが、デザイナのウィンドウ内では、既にコントロール (ここではグリッド) として制御されていることです。グリッドは、もちろんサイズを変更したり移動したりすることができます。追加するコントロールはこの中にネストされ、コントロールの Position プロパティの値は、格納されるウィンドウではなく、このグリッドに関連付けられます。ここで、ListBox をツールボックスからグリッドにドラッグし、"WindowList" という名前を付けます。グリッドの左側に移動するように、サイズを変更します。TabIndex プロパティを "1" に設定し、SelectionMode プロパティを "Multiple" に設定します。このリスト ボックスが、ウィンドウ タイトルを格納する一覧になります。

次に、グリッドの右側に 4 つのボタンを追加します。TabIndex の値をそれぞれ 2、3、4、5 に設定し、コードを設定するボタンの名前をそれぞれ GitOverHereBtn、RefreshBtn、SelectAllBtn、ClearAllBtn とします。同様にタイトルも必要なので、Content プロパティを使用してボタンのタイトルを設定します。フォーム コントロールの場合に使用する Text プロパティはありません。

ヘルパーコードを追加する

いよいよコードに移ります。ウィンドウ (グリッドではなく、ウィンドウ自体) を選択してダブルクリックします。コードエディタが表示されるので、Window1_Loaded メソッド (別の名前を付ける場合はそのメソッド) を編集します。このメソッドは、フォームの場合の "Form1_Load" メソッドに相当します。このメソッドで実行するのは、リスト ビューを作成することだけです。新しいウィンドウが開いたり既存のウィンドウが閉じたりした場合に更新する手段も必要なため、ヘルパーメソッドを記述してこれを実行します。その前に、オペレーティング システムからウィンドウの一覧を取得する際に使用するため、Windows API をいくつか宣言する必要があります。これを実行するには、次の宣言をクラス内に追加します。

Declare Auto Function SetWindowPos Lib "user32.dll" (ByVal hWnd As IntPtr, _

ByVal hWndAfter As IntPtr, ByVal X As Integer, ByVal Y As Integer, _

ByVal CX As Integer, ByVal CY As Integer, ByVal uFlags As UInteger) As Integer

 

これで、作成するアプリケーションのウィンドウ以外のウィンドウの位置を設定できます。

Delegate Function EnumWindowsCallback(ByVal hWnd As IntPtr, _

ByVal lParam As Integer) As Boolean

 

Declare Ansi Function EnumChildWindows Lib "user32.dll" (ByVal hwnd As IntPtr, _

ByVal MyCallBack As EnumWindowsCallback, ByVal lParam As Integer) As Boolean

 

この 2 行のコードで、コンピュータ上に現在あるすべてのウィンドウの一覧をオペレーティングシステムから取得できます。EnumChildWindows を呼び出して初期化し、見つかったウィンドウと必要な情報に対し、各ウィンドウにつきデリゲートされた関数を呼び出します。

Declare Ansi Function GetWindowTextA Lib "user32.dll" (ByVal hwnd As IntPtr, _

ByVal Str As StringBuilder, ByVal lSize As Integer) As Integer

 

これで、作成するアプリケーションのウィンドウ以外のウィンドウのタイトルバー テキストを取得できます。

Declare Function GetWindowLongA Lib "user32.dll" (ByVal hwnd As IntPtr, _

ByVal num As Integer) As Integer

 

このコードで、作成するアプリケーションのウィンドウ以外のウィンドウの全般プロパティを取得できるため、扱う必要がない種類のウィンドウを一覧から除外できます。

次に、これらの API で使用する定数をいくつか定義する必要があるので、上記の行の直後に次の行を追加します。

Public Const SWP_NOSIZE = &H1

Public Const SWP_NOZORDER = &H4

Const WS_VISIBLE = &H10000000

Const WS_BORDER = &H800000

Const GWL_STYLE = -16

 

これで、次のヘルパー関数を作成する準備ができました。

    Private Sub UpdateWindowList()

        Me.WindowList.Items.Clear()

        Me.WindowList.SelectedIndex = -1

        Me.GitOverHereBtn.IsEnabled = False

 

        EnumChildWindows(0, AddressOf ChildWinCallback, 0)

    End Sub

 

このメソッドの基本的な内容は、リストビューで選択されている項目がないようにビューをクリアし (実のところ、この記述は余計ですが、安全を期すため日ごろこのようにする習慣なのです)、ウィンドウを移動するためのボタンを無効にします。選択項目がある場合にのみ有効にする必要があるからです。その後、肝心のウィンドウを取得するため、上記で宣言した列挙子を呼び出します。このコールバックは、上記で定義したデリゲートのシグネチャに合わせて次のように宣言します。

    Function ChildWinCallback(ByVal lhWnd As IntPtr, ByVal lParam As Integer) As Boolean

 

これで、渡されたウィンドウ ハンドルとそのタイトルを取得でき、タイトルをリストにスローします。しかし、ハンドルに対応するウィンドウ タイトルが選択されたときに、このハンドルを後で使用するためにキャッシュする方法が必要になるので、その情報をキャッシュする子クラスを次のように作成します。

    Class WinWrapper

        Sub New(ByVal h As IntPtr, ByVal c As String)

            Hwnd = h

            Caption = c

        End Sub

        Overrides Function ToString() As String

            Return Caption

        End Function

        Public Hwnd As IntPtr

        Public Caption As String

    End Class

 

(この実装では、このクラスを全体ウィンドウクラスに組み込んでいますが、上記のほかに用途がないからです。) このクラスは、ウィンドウハンドルとウィンドウキャプションの両方をキャッシュし、"ToString" を介してキャプションを公開します。これにより、このクラスのオブジェクトから文字列を取得すると、このキャプションが返されます。

ここまでくれば、ウィンドウタイトルを取得してリストビューにスローするのは簡単です。ChildWinCallback は次のようになります。

    Function ChildWinCallback(ByVal lhWnd As IntPtr, ByVal lParam As Integer) As Boolean

            Dim Caption As New StringBuilder("", 500)

            GetWindowTextA(lhWnd, Caption, Caption.Capacity)

            Dim c As String = Caption.ToString

            If c.Length > 0 Then

                Dim wrappedWindow As New WinWrapper(lhWnd, c)

                Me.WindowList.Items.Add(wrappedWindow)

            End If

        Return True

    End Function

 

このように、wrappedWindow オブジェクトをリスト ビューに追加すると、ToString を介してウィンドウ テキストが取得されます。

ここで、上記のコードは、実際にタイトルが付けられているウィンドウのみを追加することに注意してください。タイトルの長さが 0 を超えることをコード内でチェックしているからです。しかし、ウィンドウの中には、これら以外のリストから除外してよいものが含まれる可能性があります。ウィンドウ情報の取得に関し、Calvin Hsia が優れたブログ記事 (英語) を書いています。ウィンドウ テキストを上記のコードの StringBuilder で取得する方法は、ここからアイデアを拝借しています。ウィンドウが表示されているかどうかを確認するコードも彼の手法をお手本にして、上記で宣言した GetWindowLongA メソッドの使用を次のように追加します。

        Dim nStyle = GetWindowLongA(lhWnd, GWL_STYLE)

        If (nStyle And (WS_VISIBLE + WS_BORDER)) = (WS_VISIBLE + WS_BORDER) Then

            Dim Caption As New StringBuilder("", 500)

            GetWindowTextA(lhWnd, Caption, Caption.Capacity)

            Dim c As String = Caption.ToString

            If c.Length > 0 Then

                Dim wrappedWindow As New WinWrapper(lhWnd, c)

                Me.WindowList.Items.Add(wrappedWindow)

            End If

        End If

 

ウィンドウのスタイルに枠線が含まれない、またはウィンドウが表示されていない場合、それらのウィンドウは移動する必要がないと見なして除外します。

イベントを捕捉する

下準備は以上です。では、これを活用してハンドラをすべてのボタンに追加します。フォームのボタンと同様に、ボタンをダブルクリックするだけで "Click" イベントのハンドラを編集する箇所にジャンプします。余談ですが、切り替えてデザイナに戻るときに、エディタの変更を反映するため、更新するかどうかの確認が表示されます。更新の必要がある場合、デザイナの最上部に金色のステータス バーが表示されることでわかります。金色のバーをクリックすると、更新が実行されます。

コードが開いたら、Refresh ボタンのイベントを処理します。この方法は簡単で、上記で定義したヘルパー関数を次のように呼び出すだけです。

    Private Sub RefreshBtn_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles RefreshBtn.Click

        UpdateWindowList()

    End Sub

 

SelectAll と ClearAll についても、次のように単純なコードになります。

    Private Sub ClearAll_Click(ByVal sender As System.Object, _

ByVal e As System.Windows.RoutedEventArgs) Handles ClearAll.Click

        Me.WindowList.SelectedIndex = -1

    End Sub

 

    Private Sub SelectAll_Click(ByVal sender As System.Object, _

ByVal e As System.Windows.RoutedEventArgs) Handles SelectAll.Click

        Me.WindowList.SelectAll()

    End Sub

 

あとは、この演習の要点である "GitOverHere" ボタンだけになりました。次のコードは、リストビュー内で選択された各オブジェクトに対し、ラップするウィンドウ上で SetWindowPos を呼び出す処理を繰り返し実行します。

    Private Sub GitOverHereBtn_Click(ByVal sender As System.Object, _

ByVal e As System.Windows.RoutedEventArgs) Handles GitOverHereBtn.Click

        For Each wrappedWindow As WinWrapper In Me.WindowList.SelectedItems

            If SetWindowPos(wrappedWindow.Hwnd, Nothing, 10, 10, -1, -1, _

SWP_NOSIZE Or SWP_NOZORDER) = False Then

                MsgBox(String.Format(My.Resources.WindowError, Me.Title))

            End If

        Next

    End Sub

 

このコードでは、各ウィンドウをサイズや Z 順 (つまり、他のウィンドウとの相対的順序) を変更せずに (10,10) に移動するよう命令します。そのため、-1, -1 をサイズ座標として渡し、hWndAfter のパラメータとして Nothing を渡します。呼び出しに失敗した場合 (ウィンドウが現在ない場合に起こる可能性がある)、メッセージ ボックスに文字列 (My.Resources.WindowError) を表示します。私が使用した文字列は次のとおりです。

ウィンドウを移動できませんでした。このウィンドウのアプリケーションは既に終了している可能性があります。{0} ウィンドウの [Refresh] をクリックし、現在使用できるウィンドウを表示してください。

String.Format の呼び出しによって、{0} は "VBGitOverHere" アプリケーションのタイトルに置き換えられます。念のため書いておくと、プロジェクトに文字列リソースを追加するには、プロジェクトを右クリックし、[プロパティ] をクリックし、表示されるウィンドウの [リソース] タブをクリックします。このタブの左上隅が "Strings" に設定されていることを確認し、タイトル文字列をそのタブのグリッドに追加します。これらは "My.Resources" 名前空間から参照できます。

最後に残った処理は、"GitOverHere" ボタンの有効化/無効化を制御することです。コードエディタの最上部で、"WindowList" のドロップダウンリストを左側に、"SelectionChanged" のドロップダウン リストを右側に設定します。これで該当するハンドラが生成され、このハンドラに次の行を追加します。

    Private Sub WindowList_SelectionChanged(ByVal sender As Object, _

ByVal e As System.Windows.Controls.SelectionChangedEventArgs) _

Handles WindowList.SelectionChanged

        Me.GitOverHereBtn.IsEnabled = (Me.WindowList.SelectedItems.Count > 0)

    End Sub

 

選択項目が変更されると、選択項目が残っている場合は有効になり、ない場合は無効になって毎回ボタンの有効化と無効化が切り替わります (フォーム コントロールの場合、"Enabled" プロパティを "IsEnabled" プロパティの代わりに使用します)。

これで完成です。このアプリケーションを実行すると、システム上のタイトルが付いて表示されており、枠線があるすべてのウィンドウの一覧が表示されます。任意のタイトルまたはすべてのタイトルを選択し、[Git Over Here] ボタンをクリックすると、ウィンドウが第 1 モニタの左端に移動します。このプログラムは、並べて表示するなど、ウィンドウを別の方法で制御するように簡単に書き換えることができます (ウィンドウをプログラムで制御するその他の方法については、Calvin のブログ (英語) を参照することをお勧めします)。

付記

このソリューション (英語) 全体は、コード ギャラリーの私のサイト (英語) に投稿してあります。ご自由にダウンロードしてください。これから 2、3 日休暇に入りますので、投稿にコメントをいただいてもすぐにはお返事できません。戻ったら 4 月中旬にできるだけ早くフォローしますのでご容赦ください。

次回をお楽しみに。

--Matt--*

VB チーム

投稿 : 2008 年 4 月 3 日 11:50 AM

分類 : Matt GertzVB2008WPF

VB チームの Web ログ - https://blogs.msdn.com/vbteam/archive/2008/04/03/git-over-here-making-your-windows-mind-their-manners-matt-gertz.aspx (英語) より