Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF WindowFinder Tool

0.00/5 (No votes)
23 Apr 2010 1  
A simple tool giving a nice overview about WPF techniques

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.

/// <summary>
/// Item view model for one item in the window list.
/// All coordinates are native window coordinates, it's up to the view 
/// to translate this properly.
/// </summary>
[SecurityPermission(SecurityAction.LinkDemand)]
public class WindowItemViewModel : INotifyPropertyChanged
{
    /// <summary>
    /// Create a new object for the window with the given handle.
    /// </summary>
    /// <param name="handle">The handle of the window.</param>
    public WindowItemViewModel(IntPtr handle)
    {
        this.Handle = handle;
    }
    
    /// <summary>
    /// The handle of this window.
    /// </summary>
    public IntPtr Handle
    {
        get;
        private set;
    }
    
    /// <summary>
    /// The window text of this window.
    /// </summary>
    public string Text
    {
        get
        {
            StringBuilder stringBuilder = new StringBuilder(256);
            NativeMethods.GetWindowText(Handle, stringBuilder, stringBuilder.Capacity);
            return stringBuilder.ToString();
        }
    }
    
    /// <summary>
    /// The process image file path.
    /// </summary>
    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();
        }
    }
    
    /// <summary>
    /// The windows rectangle.
    /// </summary>
    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.

/// <summary>
/// View model for the TopLevelWindowsView. 
/// Provide a list of top level windows and some information about the space they occupy.
/// All coordinates are native window coordinates, 
/// it's up to the view to translate this properly.
/// </summary>
public class TopLevelWindowsViewModel : DependencyObject
{
    /// <summary>
    /// Total width needed to display all windows.
    /// </summary>
    public double TotalWidth
    {
        get { return (double)GetValue(TotalWidthProperty); }
        set { SetValue(TotalWidthProperty, value); }
    }
    public static readonly DependencyProperty TotalWidthProperty =
        DependencyProperty.Register
	("TotalWidth", typeof(double), typeof(TopLevelWindowsViewModel));
        
    /// <summary>
    /// Total height needed to display all windows.
    /// </summary>
    public double TotalHeight
    {
        get { return (double)GetValue(TotalHeightProperty); }
        set { SetValue(TotalHeightProperty, value); }
    }
    public static readonly DependencyProperty TotalHeightProperty =
        DependencyProperty.Register
	("TotalHeight", typeof(double), typeof(TopLevelWindowsViewModel));
        
    /// <summary>
    /// Margin needed to ensure all windows are displayed within the scrollable area.
    /// </summary>
    public Thickness TotalMargin
    {
        get { return (Thickness)GetValue(TotalMarginProperty); }
        set { SetValue(TotalMarginProperty, value); }
    }
    public static readonly DependencyProperty TotalMarginProperty =
        DependencyProperty.Register
	("TotalMargin", typeof(Thickness), typeof(TopLevelWindowsViewModel));
        
    /// <summary>
    /// All top level windows to display.
    /// </summary>
    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));
        
    /// <summary>
    /// Refresh all properties.
    /// </summary>
    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);
            
        // Now calculate total bounds of all windows
        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:

Snoop1

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 ContentPresenters 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.

/// <summary>
/// Interaction logic for TopLevelWindowsView.xaml
/// </summary>
public partial class TopLevelWindowsView : UserControl
{
    public TopLevelWindowsView()
    {
        InitializeComponent();
        Application.Current.MainWindow.Activated += MainWindow_Activated;
    }
    
    /// <summary>
    /// The associated view model.
    /// </summary>
    private TopLevelWindowsViewModel ViewModel
    {
        get
        {
            return DataContext as TopLevelWindowsViewModel;
        }
    }
    
    private void MainWindow_Activated(object sender, EventArgs e)
    {
        // Refresh the view every time  the main window is activated - 
        // other windows positions might have changed.
        ViewModel.Refresh();
    }
    
    protected override void OnKeyDown(KeyEventArgs e)
    {
        base.OnKeyDown(e);
        
        // Provide a keyboard shortcut to manually refresh the view.
        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())
        {
            // Moving our own window would cause strange flickering, don't allow this.
            return;
        }
        
        // Capture the mouse and connect to the elements mouse move & button up events.
        caption.CaptureMouse();
        caption.MouseLeftButtonUp += caption_MouseLeftButtonUp;
        caption.MouseMove += caption_MouseMove;
        
        // Remember the current mouse position and window rectangle.
        this.dragStartMousePos = e.GetPosition(caption);
        this.dragWindowRect = window.Rect;
    }
    
    private void caption_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        FrameworkElement caption = (FrameworkElement)sender;
        
        // Stop dragging - release capture, disconnect events and refresh the view.
        caption.ReleaseMouseCapture();
        caption.MouseLeftButtonUp -= caption_MouseLeftButtonUp;
        caption.MouseMove -= caption_MouseMove;
        
        ViewModel.Refresh();
    }
    
    private void caption_MouseMove(object sender, MouseEventArgs e)
    {
        FrameworkElement caption = (FrameworkElement)sender;
        
        // Move the dragged window:
        Point mousePos = e.GetPosition(caption);
        Vector delta = mousePos - this.dragStartMousePos;
        dragWindowRect.Offset(delta);
        
        // Apply changes to view model.
        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

  • 2010/04/20 V 1.0

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here