Introduction
Window finder is a simple WPF application that shows all the visible top level windows of the current session. It can be very useful to get back access to windows that have gone off screen, e.g. after you have undocked your laptop. While being off screen is not a real problem for top level windows - you can still use the task bar context menu to move them - it can be for popup windows that don't appear in the task bar, especially if the application remembers the position and insists on drawing it outside of the visible area even after a restart. With this little tool, you can just drag any window back to the visible area.
The functionality of this tool is not a big thing, but it nicely shows some WPF techniques in a concise project and how easy it is with WPF to have a simple implementation for simple things, so I decided to publish it here.
What Can I Get from this Article?
The WPF techniques covered here are:
- MVVM design
- Using an
ItemsControl
with a Canvas
as the items panel
- Using a
ViewBox
to decouple View
and ViewModel
coordinates
- Dragging items inside of a
ViewBox
Using the Code
Like all my WPF apps, I built this little application using the MVVM design. Even for such simple apps, it pays off using it since it really helps to keep everything simple and clear.
To have all information required to display the items, only two view models are needed: one representing each window and one holding the collection of all windows. The coordinates used in the view models are the native windows coordinates; we don't need any view related translations in the view model.
The WindowItemViewModel
represents each window and has just Handle, Text, Process
and Rect
properties. That's all we need to display a placeholder of a window. It implements INotifyPropertyChanged
so the view of the window will follow the real windows position when we change it. You may notice that the properties like Text
or Process
are not buffered, but will get evaluated every time the property getter is called. This is OK for properties designed to be used in binding, since the binding will anyhow call the getter just once, so buffering would be an unnecessary overhead. And if evaluating the property turns out to be slow, you could just make the binding asynchronous to speed up the display, which would not work if you had evaluated it in the constructor to feed the property with the value.
[SecurityPermission(SecurityAction.LinkDemand)]
public class WindowItemViewModel : INotifyPropertyChanged
{
public WindowItemViewModel(IntPtr handle)
{
this.Handle = handle;
}
public IntPtr Handle
{
get;
private set;
}
public string Text
{
get
{
StringBuilder stringBuilder = new StringBuilder(256);
NativeMethods.GetWindowText(Handle, stringBuilder, stringBuilder.Capacity);
return stringBuilder.ToString();
}
}
public string Process
{
get
{
IntPtr processIdPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(int)));
NativeMethods.GetWindowThreadProcessId(Handle, processIdPtr);
int processId = (int)Marshal.PtrToStructure(processIdPtr, typeof(int));
IntPtr hProcess = NativeMethods.OpenProcess
(NativeMethods.PROCESS_ALL_ACCESS, false, processId);
var stringBuilder = new StringBuilder(256);
NativeMethods.GetProcessImageFileName
(hProcess, stringBuilder, stringBuilder.Capacity);
NativeMethods.CloseHandle(hProcess);
Marshal.FreeCoTaskMem(processIdPtr);
return stringBuilder.ToString();
}
}
public Rect Rect
{
get
{
NativeMethods.Rect nativeRect;
NativeMethods.GetWindowRect(Handle, out nativeRect);
Rect windowRect = new Rect(nativeRect.TopLeft(), nativeRect.BottomRight());
return windowRect;
}
set
{
NativeMethods.SetWindowPos(Handle, IntPtr.Zero,
(int)(value.Left),
(int)(value.Top),
0, 0, NativeMethods.SWP_NOACTIVATE |
NativeMethods.SWP_NOSIZE | NativeMethods.SWP_NOZORDER);
ReportPropertyChanged("Rect");
}
}
#region INotifyPropertyChanged Members
private void ReportPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
The TopLevelWindowsViewModel
provides a list of all windows to show, plus the bounding rectangle of all windows to set up proper scrolling. It derives from DependencyObject
so we can just use dependency properties. Looking at the code dependency properties seem to introduce a lot of typing work, but if you use the "propdp" code snippet, it turns out to be easier to handle than a simple property with INotifyPropertyChanged
- and much safer! The only "written" code here is the Refresh
method, that simply enumerates all windows, creates the list of item view models and calculates the bounds.
public class TopLevelWindowsViewModel : DependencyObject
{
public double TotalWidth
{
get { return (double)GetValue(TotalWidthProperty); }
set { SetValue(TotalWidthProperty, value); }
}
public static readonly DependencyProperty TotalWidthProperty =
DependencyProperty.Register
("TotalWidth", typeof(double), typeof(TopLevelWindowsViewModel));
public double TotalHeight
{
get { return (double)GetValue(TotalHeightProperty); }
set { SetValue(TotalHeightProperty, value); }
}
public static readonly DependencyProperty TotalHeightProperty =
DependencyProperty.Register
("TotalHeight", typeof(double), typeof(TopLevelWindowsViewModel));
public Thickness TotalMargin
{
get { return (Thickness)GetValue(TotalMarginProperty); }
set { SetValue(TotalMarginProperty, value); }
}
public static readonly DependencyProperty TotalMarginProperty =
DependencyProperty.Register
("TotalMargin", typeof(Thickness), typeof(TopLevelWindowsViewModel));
public IEnumerable<WindowItemViewModel> TopLevelWindows
{
get { return (IEnumerable<WindowItemViewModel>)
GetValue(TopLevelWindowsProperty); }
set { SetValue(TopLevelWindowsProperty, value); }
}
public static readonly DependencyProperty TopLevelWindowsProperty =
DependencyProperty.Register("TopLevelWindows",
typeof(IEnumerable<WindowItemViewModel>), typeof(TopLevelWindowsViewModel));
internal void Refresh()
{
List<WindowItemViewModel> topLevelWindows = new List<WindowItemViewModel>();
NativeMethods.EnumWindows(new NativeMethods.WNDENUMPROC(
delegate(IntPtr hwnd, IntPtr lParam)
{
if (NativeMethods.IsWindowVisible(hwnd))
{
long style = NativeMethods.GetWindowLong
(hwnd, NativeMethods.GWL_STYLE);
if ((style & (NativeMethods.WS_MAXIMIZE |
NativeMethods.WS_MINIMIZE)) == 0)
{
topLevelWindows.Add(new WindowItemViewModel(hwnd));
}
}
return 1;
})
, IntPtr.Zero);
double xMin = topLevelWindows.Select(window => window.Rect.Left).Min();
double xMax = topLevelWindows.Select(window => window.Rect.Right).Max();
double yMin = topLevelWindows.Select(window => window.Rect.Top).Min();
double yMax = topLevelWindows.Select(window => window.Rect.Bottom).Max();
TotalWidth = xMax - xMin;
TotalHeight = yMax - yMin;
TotalMargin = new Thickness(-xMin, -yMin, xMin, yMin);
TopLevelWindows = topLevelWindows;
}
}
Now that the view models are set up, we can start designing the view:
<UserControl x:Class="WindowFinder.TopLevelWindowsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:WindowFinder"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Background="AliceBlue"
>
<UserControl.DataContext>
<src:TopLevelWindowsViewModel/>
</UserControl.DataContext>
<DockPanel>
<Grid DockPanel.Dock="Bottom">
<Slider x:Name="zoom" Margin="15,5" HorizontalAlignment="Right"
Width="200" Minimum="0.1" Maximum="2" Value="1"/>
</Grid>
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<Viewbox Width="{Binding TotalWidth}" Height="{Binding TotalHeight}">
<Viewbox.LayoutTransform>
<ScaleTransform ScaleX="{Binding Value, ElementName=zoom}"
ScaleY="{Binding Value, ElementName=zoom}"/>
</Viewbox.LayoutTransform>
<ItemsControl ItemsSource="{Binding TopLevelWindows}"
Width="{Binding TotalWidth}" Height="{Binding TotalHeight}"
Margin="{Binding TotalMargin}">
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Rect.Left}"/>
<Setter Property="Canvas.Top" Value="{Binding Rect.Top}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border SnapsToDevicePixels="True" Width="{Binding Rect.Width}"
Height="{Binding Rect.Height}"
Background="{x:Null}" BorderThickness="1" BorderBrush="Blue"
CornerRadius="2" MinHeight="10" MinWidth="50"
ToolTip="{Binding Process}" >
<TextBlock HorizontalAlignment="Stretch" VerticalAlignment="Top"
Background="Blue" Foreground="White"
Text="{Binding Text}" FontSize="8"
MouseLeftButtonDown="caption_MouseLeftButtonDown"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Viewbox>
</ScrollViewer>
</DockPanel>
</UserControl>
Since we use the MVVM pattern, the control's data context is the TopLevelWindowsViewModel
. We want to display a list of items, so we use an ItemsControl
and bind the ItemsSource
property to the TopLevelWindows
property of the view model. We want to explicitly position the items, so we use a Canvas
as the item control's ItemsPanel
. To visualize each window, we simply draw a border and a top aligned text block, so that's how the ItemTemplate
is done. The data context of each item will be a WindowItemViewModel
, so we can bind to its properties. The borders width and height simply bind to the window rectangle's width and height, but if you look at the XAML you won't find the Canvas.Left
and Canvas.Top
binding at the borders level. Why this wouldn't work you can see if you look at the visual tree, e.g. using Snoop:
The Border
, which is the root element of the item template, is not a direct child of the Canvas
. The ItemsControl
creates a list of ContentPresenter
s which then contain the item that we have designed. This seems to be a problem, since the ContentPresenter
is not part of our XAML but generated by the ItemsControl
when realizing the items, so it's not under our direct control. We could start writing code behind to bubble up the visual tree and find the ContentPresenter
or write an ItemsContainerGenerator
, but there is a much simpler solution - just using the ItemContainerStyle
to access the ContentPresenter
's properties as you can see in the XAML above.
Now we can display all our windows in the items presenter, but it would be a one to one mapping of the screen (we used native window coordinates in the view model), which is not the best for getting an overview, so we want to scale down everything. A ViewBox
will do exactly what we need. We just have to take care that we don't mess up the scrolling, so we need to keep the aspect ratio of the ViewBox
the same as the ItemsControl
's. We could use a converter in the binding to scale the values down, but this would make a dynamic scaling hard, if not impossible. The easiest way to solve this in XAML without writing extra code is to simply bind the native width and height to the ViewBox
and then just scale the whole view box using a layout transformation.
The final step is to add a simple drag handler to support dragging the lost windows back to the visible area. Just dragging items is a simple task; remember the initial mouse position in the mouse down event and measure the distance in each mouse move event. Since the dragging is applied against the view model and the view model is using native window coordinates, everything turns out to match perfect.
public partial class TopLevelWindowsView : UserControl
{
public TopLevelWindowsView()
{
InitializeComponent();
Application.Current.MainWindow.Activated += MainWindow_Activated;
}
private TopLevelWindowsViewModel ViewModel
{
get
{
return DataContext as TopLevelWindowsViewModel;
}
}
private void MainWindow_Activated(object sender, EventArgs e)
{
ViewModel.Refresh();
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.Key == Key.F5)
{
ViewModel.Refresh();
}
}
#region Simple drag handler
private Point dragStartMousePos;
private Rect dragWindowRect;
private void caption_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
FrameworkElement caption = (FrameworkElement)sender;
WindowItemViewModel window = (WindowItemViewModel)caption.DataContext;
if (window.Handle == this.GetHandle())
{
return;
}
caption.CaptureMouse();
caption.MouseLeftButtonUp += caption_MouseLeftButtonUp;
caption.MouseMove += caption_MouseMove;
this.dragStartMousePos = e.GetPosition(caption);
this.dragWindowRect = window.Rect;
}
private void caption_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
FrameworkElement caption = (FrameworkElement)sender;
caption.ReleaseMouseCapture();
caption.MouseLeftButtonUp -= caption_MouseLeftButtonUp;
caption.MouseMove -= caption_MouseMove;
ViewModel.Refresh();
}
private void caption_MouseMove(object sender, MouseEventArgs e)
{
FrameworkElement caption = (FrameworkElement)sender;
Point mousePos = e.GetPosition(caption);
Vector delta = mousePos - this.dragStartMousePos;
dragWindowRect.Offset(delta);
WindowItemViewModel window = (WindowItemViewModel)caption.DataContext;
window.Rect = dragWindowRect;
}
#endregion
}
That's all coding. Just plug the view into the main window and the application is ready - you never need to suffer from lost popup windows again.
History