WPF 窗口查找工具






4.67/5 (8投票s)
一个简单的工具,提供了 WPF 技术的良好概述。
引言
窗口查找器是一个简单的WPF应用程序,它显示当前会话中所有可见的顶层窗口。当您的笔记本电脑被坞站分离后,有些窗口可能移出了屏幕,此时该工具可以帮助您找回这些窗口的访问权限。对于顶层窗口而言,屏幕外并不是一个真正的问题——您仍然可以使用任务栏上下文菜单来移动它们——但对于不出现在任务栏上的弹出窗口来说,这可能是一个问题,尤其是当应用程序记住其位置并在重新启动后仍坚持将其绘制在可见区域之外时。有了这个小工具,您就可以轻松地将任何窗口拖回到可见区域。
这个工具的功能本身并不复杂,但它以一个简洁的项目很好地展示了一些WPF技术,以及使用WPF实现简单事物是多么容易,所以我决定在这里发布它。
我能从这篇文章中学到什么?
本文涵盖的WPF技术包括:
- MVVM 设计
- 使用 `ItemsControl` 并以 `Canvas` 作为其项目面板
- 使用 `ViewBox` 来解耦 `View` 和 `ViewModel` 的坐标
- 在 `ViewBox` 中拖动项目
Using the Code
与我所有的WPF应用一样,我使用MVVM设计构建了这个小程序。即使对于这样简单的应用,使用它也是值得的,因为它确实有助于保持一切简单明了。
为了显示项目所需的所有信息,只需要两个视图模型:一个代表每个窗口,另一个包含所有窗口的集合。视图模型中使用的坐标是原生窗口坐标;我们不需要在视图模型中进行任何与视图相关的转换。
`WindowItemViewModel` 代表每个窗口,只包含 `Handle`, `Text`, `Process` 和 `Rect` 属性。这些是我们显示窗口占位符所需的所有信息。它实现了 `INotifyPropertyChanged`,因此当您更改窗口的位置时,视图会跟随实际窗口的位置。您可能会注意到 `Text` 或 `Process` 等属性没有进行缓存,而是在每次调用属性getter时进行评估。这对于用于绑定的属性来说是没问题的,因为绑定无论如何都只调用一次getter,所以缓存会造成不必要的开销。而且,如果评估属性发现很慢,您可以将绑定设置为异步以加快显示速度,这在构造函数中评估并为属性提供值的情况下是行不通的。
/// <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
}
`TopLevelWindowsViewModel` 提供了一个要显示的所有窗口列表,以及所有窗口的边界矩形,用于设置适当的滚动。它派生自 `DependencyObject`,因此我们可以直接使用依赖属性。查看代码时,依赖属性似乎引入了大量的手写工作,但如果您使用“propdp”代码片段,它会比带有 `INotifyPropertyChanged` 的简单属性更容易处理——并且更安全!这里唯一的“书写”代码是 `Refresh` 方法,它只是枚举所有窗口,创建项目视图模型列表并计算边界。
/// <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;
}
}
现在视图模型已经设置好了,我们可以开始设计视图了。
<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>
由于我们使用MVVM模式,控件的数据上下文是 `TopLevelWindowsViewModel`。我们想显示一个项目列表,所以我们使用 `ItemsControl` 并将 `ItemsSource` 属性绑定到视图模型的 `TopLevelWindows` 属性。我们想明确地定位项目,所以我们使用 `Canvas` 作为 `ItemsControl` 的 `ItemsPanel`。为了可视化每个窗口,我们只需绘制一个边框和一个顶部对齐的文本块,这就是 `ItemTemplate` 的实现方式。每个项目的上下文是 `WindowItemViewModel`,所以我们可以绑定到它的属性。边框的宽度和高度直接绑定到窗口矩形的宽度和高度,但如果您查看XAML,您不会在边框级别找到 `Canvas.Left` 和 `Canvas.Top` 绑定。为什么这行不通,您可以查看视觉树,例如使用Snoop。

作为项目模板根元素的 `Border` 不是 `Canvas` 的直接子元素。`ItemsControl` 创建了一个 `ContentPresenter` 列表,然后这些 `ContentPresenter` 包含我们设计的项目。这似乎是个问题,因为 `ContentPresenter` 不是我们XAML的一部分,而是由 `ItemsControl` 在实现项目时生成的,因此我们无法直接控制它。我们可以开始编写代码隐藏来向上遍历视觉树并找到 `ContentPresenter`,或者编写一个 `ItemsContainerGenerator`,但有一个更简单的解决方案——只需使用 `ItemContainerStyle` 来访问 `ContentPresenter` 的属性,如您在上面的XAML中所见。
现在我们可以在项目展示器中显示所有窗口了,但这将是一个屏幕的一比一映射(我们在视图模型中使用了原生窗口坐标),这对于获得整体概览并不是最好的,所以我们想缩小一切。`ViewBox` 将正好完成我们所需的工作。我们只需要注意不要弄乱滚动,所以我们需要保持 `ViewBox` 的宽高比与 `ItemsControl` 相同。我们可以使用绑定中的转换器来缩小值,但这将使动态缩放变得困难,甚至不可能。在XAML中不编写额外代码的最简单解决方法是简单地将原生宽度和高度绑定到 `ViewBox`,然后使用布局转换来缩放整个 `ViewBox`。
最后一步是添加一个简单的拖动处理程序,以支持将丢失的窗口拖回到可见区域。仅仅拖动项目是一项简单的任务;在鼠标按下事件中记住初始鼠标位置,并在每次鼠标移动事件中测量距离。由于拖动是针对视图模型应用的,并且视图模型使用的是原生窗口坐标,所以一切都完美匹配。
/// <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
}
代码就这些了。只需将视图插入主窗口,应用程序就准备就绪了——您再也不必忍受丢失弹出窗口的痛苦了。
历史
- 2010/04/20 V 1.0