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

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

  • Comments 16

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

    End Function

 

When you add the wrappedWindow object to the listview, it will use ToString to get the window text.

Now, note that code above only adds windows that actually have a title, because it checks to make sure that the title length is greater than zero.  However, there may be other windows you don’t care about.  Calvin Hsia has an excellent blog post on retrieving window information, and I’ve already borrowed one idea from it -- the cool use of StringBuilder above to get the window text.  I’m also following in his footsteps to check for the visibility of a window, which can be done by adding use of the GetWindowLongA method which was declared above:

        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

 

In other words, if the style of the window doesn’t include a border or isn’t visible, it’s probably not something you care about.

Hooking in the events

That’s all of the set-up work, now you can leverage it and add handlers to all of the buttons.  As with buttons on forms, you just double-click the button to take you to a place where you can edit the handler for “Click”.  (As an aside, whenever you switch back to the designer, it may prompt you to refresh it, due to changes in the editor.  If it needs to do this, it will tell you so via a gold status bar at the top of the designer – clicking on the gold bar will cause the refresh to happen.)

Code-wide, for Refresh, it’s very simple – you just call the helper function we defined above:

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

        UpdateWindowList()

    End Sub

 

For SelectAll and ClearAll, it’s also straightforward:

    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

 

That just leaves the “GitOverHere” button, which was the whole point of this exercise.  The solution here is to iterate over each selected object in the listview and call SetWindowPos on the window it wraps:

    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

 

In this code, you’re telling each window to move to (10,10), and not to change its size nor its z-order (i.e., its depth relative to other windows).  You can thus pass in -1, -1 for the size coordinates, and Nothing for the hWndAfter parameter.  If the call fails (as it might if the window no longer exists), you’ll be popping up a message box showing a string (My.Resources.WindowError). The one I use is:

The window could not be moved -- its application may have already exited.  Choose 'Refresh' in the {0} window to see the currently available windows.

The String.Format call will replace the {0} with the title of the application, “VBGitOverHere”.  (As a reminder:  to add string resources to a project, right-click the project, choose “Properties,” select the “Resources” tab in the resulting window, make sure the upper-left corner of the tab is set to “Strings,” and add string titles and values in the grid on that tab.  You can reference them in code via the “My.Resources” namespace.)

There’s one last thing to do:   you need to control the enabling/disabling of the “GitOverHere” button.  At the top of the code editor, set the left-hand dropdown to “WindowList” and the right-hand one to “SelectionChanged.”  That will generate the appropriate handler, and you’ll add one line to it:

    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

 

Now, every time the selection is changed, we’ll enable the button if there are any selections left, or disable it otherwise.  (Note that for Forms controls, we’d be using the “Enabled” property instead of the “IsEnabled” property.)

That’s it, you’re done!  When you launch the app, you’ll be presented with a list of all of the titled, visible, bordered windows on the system.  You can select any or all of them and click “Git Over Here,” and the windows will jump over to the leftmost part of the first screen.  You could easily modify this program to tile them as well, or control windows in other ways.  (I do urge you to check out Calvin’s blog on some other cool things you can do programmatically with windows!)

Administrivia

I’ve posted the entire solution at my site on Code Gallery, so feel free to download it from there.  Note that I’m going on vacation in a couple of days, so I won’t be able to respond to any comments on this post immediately – I’ll do my best to follow up when I get back in mid-April.

‘Til next time,

--Matt--*

Leave a Comment
  • Please add 3 and 5 and type the answer here:
  • Post
  • PingBack from http://workfromhome.poemshop.info/2008/04/03/git-over-here-making-your-windows-mind-their-manners-matt-gertz/

  • "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."

    Just right-click on the relevant window’s icon in the task bar and choose “Move”.  Then press ANY of the cursor keys ONCE.  Now that window is stuck to your mouse pointer.  Just move the window with your mouse now.  -- Works great for me and very quick to do so!

  • Thanks for the tip; I just tried it out.  For whatever reason, when connecting remotely, mine will only engage with the mouse after it's been keyed over to the first screen -- I get the funky "move" cursor, but moving the mouse does nothing unless at least one part of the window already overlaps onto the first monitor (which, in my case, I end up doing with the cursor keys).  However, it *does* seem to work like you say if I'm not connecting over remote connection (i.e., if I'm actually at the machine at work).  Very odd -- I'll have to ask my friends on the Windows team what's going on when I get a free second.

  • Joey> Works for Windows that are present in the taskbar, but what for the dialogs which appears neither in the taskbar nor in the alt-tab ?

    Namely, it is frequent for me to have Crystal Reports dialogs such as "database", "format" or "formula" to appear on my no-longer-existent right screen, which pretty much prevent any kind of task to be performed. "Hello Boss, you need this field to disappear if negative in ten minutes ? Well that's a funny story..."

  • VBTeam, Thanks for clearing that up!  I see what you mean.

    Jem, try alt+space, m

    then move the cursor with the keys once or twice

    now your mouse should have that pesky dialog stuck to it.

    Not sure about all dialog boxes, but I think all of them I've tried act like this.

  • Joey> Thanks for the tip, I'll be sure to try it next time.

  • If you don't mind scrolling, an alternative is to simply use VNC...

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

  • On behalf of the Microsoft Visual Basic team, I'd like to wish all of our readers a happy & healthy

  • On behalf of the Microsoft Visual Basic team, I'd like to wish all of our readers a happy & healthy

  • Another great acticle...

    But i must ask you something. I've been trying and trying, searching endlessly on the internet, but i can't find anything on the internet that really teaches me more. It's either easy stuff, or articles like these *gulp* that are way too far beyond me. And just experimenting takes forever, and usually doesn't show much results, because i don't know what to look for. How can i learn?

  • This is a great question, Jerry, and one that deserves a better answer than me typing in a few sentences in the comment section.  I've decided that I'll be making this the topic of a blog post later this week -- so I'll have an answer (or, at least, an opinion) in a couple of days.

    --Matt--*

  • Great! now i'll have an idea on what to do to learn :)

  • Done -- mercifully, my schedule cleared up today, and I was able to write this up during the afternoon.  Check it out!

    --Matt--*

  • Great article!

    However, this is not clear to me:

    'GitOverHereBtn_Click

           For Each wrappedWindow As WinWrapper In Me.WindowList.SelectedItems

    How's it possible that you can convert the ListBox items into WinWrappers automatically? Aren't you getting only text back from the ListBox?

Page 1 of 2 (16 items) 12