April 3rd, 2008

"Git Over Here": Making Your Windows Mind Their Manners (Matt Gertz)

Supporting the “pajama programmer”

Telecommuting is a great thing, and as the environmentally-conscious person that I strive to be, is something I wholeheartedly endorse when it can be done practically.  My current job involves a lot of face-to-face meetings with people, so I don’t actually do a lot of it.  However, I do work from home in the evenings (for example, when crafting blog posts), so it’s important to me that my setup for working from home is usable.

The challenge that I face is that my work machine is dual-monitor, whereas my home machine is single-monitor.  Consequently, unless I make a conscious decision to move all of my windows to monitor #1 before I go home, many of the windows I need are off-screen when I remotely access my work machine from home.  Choosing to “cascade” or “tile” the windows doesn’t work, since those commands don’t actually move windows from their current monitor.  I can, of course, right-click on the relevant window’s icon in the task bar and choose “Move” to slowly move the window to monitor #1 with the cursor keys, but that’s a hassle.  What I want is a quick way to say “git over here!” to one or more windows.

Now, I’m sure there is plenty of freeware out there that already does this sort of thing, but I’d also been looking for an opportunity to play around with WPF (Windows Presentation Foundation, formerly known as “Avalon”) windows, so I decided to take the time to put this together by hand as a way of learning more about them since I haven’t had too much experience with them yet.  So, note that I’m not going to be demonstrating anything “WPF-ish” in this post – no transparency or special effects or whatnot  – mostly, this exercise was just a way for me to get used to the variations in the designer and the properties before I jump in to anything trickier.

The goal is to create an application with a listview containing all of the titled windows on the system, and a button to move the selected window to location (10,10), which would certainly be on monitor #1.  (VS2005 and earlier users:  although I’ll be doing this using a WPF window in VS2008, there’s absolutely no reason this code won’t also work with forms – the translations are pretty straightforward.)

Creating the basic app – the designer

First, choose “New Project…” and select “WPF Application” from the list of Windows project types, give it a name (I chose “VBGitOverHere”) and press “OK.”  After the project is created, you’ll see a designer which is rather different looking than the one you’d be used to for forms.  At the top is something that sort of looks like the Forms designer, but at the bottom you’ll see a XAML pane.  XAML is the backbone of WPF and ultimately describes the layout of the window, rather like an ActiveX form, if you’ve used one of those before.  (We won’t actually be interacting with the XAML editor in this example, as everything I discuss in this post can be done in the designer and editor.)

The property grid for a window works pretty much the same way it does for forms.  One of the biggest differences is that the name of the object is specified at the top of the properties window containing the grid, and not in the grid itself – go ahead & change that to whatever you like.  Also, let’s change the Title property to “VBGitOverHere” (or whatever makes sense to you), Topmost to “True,” WindowStartupLocation to “Manual”, and both Left and Top to 10.  This will make sure that this window always starts up in the upper-left corner of the first monitor, on top of everything else.  (It would obviously defeat the purpose of this app to have it pop up on the second monitor, and I also want to make sure it stays on top as windows are moved around.)

One thing to notice is that the window in the designer already controls a control – a grid, which you can think of as the client area for your controls.  It can of course be resized and moved around. The other controls you add will be nested in this, and their position property values are relevant to it, not to the enclosing window.  So, drag out a ListBox from the toolbox into the grid and call it “WindowList.”  Resize it so that it takes up the left side of the grid.  Set its TabIndex property to “1” and its SelectionMode property to Multiple.  This is the list that we’ll put the window titles in.

Now add four buttons into the right side of the Grid.  You’ll want to set their TabIndex values appropriately (2, 3, 4, 5), and you’ll want to give then appropriate names (GitOverHereBtn, RefreshBtn, SelectAllBtn, ClearAllBtn) to code against.  They also need titles as well.  To set the title of the button, you use the Content property – there is no “Text” property such as Forms controls have.

Adding helper code

Now, to the code.  Select the window (not the grid – the actual window) and double-click it.  You’ll be taken to a code editor and dropped into editing the Window1_Loaded method (or whatever you called it).  This is the equivalent of the “Form1_Load” method for forms.  The only thing you’ll be doing in this method is populate the listview.  You’ll want to write a helper method for that, since you’ll also want a way to refresh the list of windows in case a new one opens or one is closed.  But before that, you’ll need to declare some Windows APIs to help me get the list of windows from the operating system.  To do this, add the following declarations inside the class:

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

 

That will allow you to set the position of a window that’s not owned by your own application.

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

 

Those two will allow you to ask the operating system for the list of all windows currently in existence on your machine.  You initiate that by calling EnumChildWindows, and that in turn calls a delegated function, once per found window, with the required information.

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

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

 

That will allow you to get the title bar text for a window that’s not owned by your own application.

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

ByVal num As Integer) As Integer

 

That will allow you to get the general properties for a window that’s not owned by your own application, so we can eliminate listing window types that we don’t actually care about.

You’ll also need to define some constants that those APIs will be expecting, so add these right after the previous lines:

Public Const SWP_NOSIZE = &H1

Public Const SWP_NOZORDER = &H4

Const WS_VISIBLE = &H10000000

Const WS_BORDER = &H800000

Const GWL_STYLE = -16

 

Now, you can create the helper function:

    Private Sub UpdateWindowList()

        Me.WindowList.Items.Clear()

        Me.WindowList.SelectedIndex = -1

        Me.GitOverHereBtn.IsEnabled = False

 

        EnumChildWindows(0, AddressOf ChildWinCallback, 0)

    End Sub

 

Basically, this method makes sure that the listview is cleared, with nothing selected (that’s actually redundant, but I tend to do it anyway for peace of mind), and that the button to move a window over is disabled, since it should only be enabled if there’s a selection.  After that’s done, it calls the enumerator we declared above to actually go and get the windows for us.  The callback for that is declared like this, matching the signature of the delegate we defined above:

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

 

Now, you could just grab the window handle that’s passed to us, get its title, and throw it into the list, but you need to have some way to cache that handle so that you can use it later when the corresponding window title is selected.  To assist with this, create a child class to cache that info:

    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

 

(In my implementation, I’ve embedded this class is in the overall window class, since I don’t have any reason for anything else to use it.)  The class caches both the window handle and the window’s caption, and then exposes the caption via “ToString” so that anything trying to get a string from an object of this class will retrieve the caption.

Getting the window’s title and throwing it into the listview are now quite easy things to do.  ChildWinCallback now looks like this:

    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

0 comments