WPF 双屏显示






4.82/5 (8投票s)
将 WPF 窗口定位在第二个显示器上。
引言
本文展示了如何在次显示器上定位 WPF 窗口或在两台显示器上显示两个窗口。本文包含完整的代码,并讨论了如何处理几种场景。
有以下免责声明:
- 我提供的代码和结论基于经验结果,因此我可能出错或描述不准确。
- 代码在具有两台显示器的计算机上进行了测试,因此三台或更多显示器可能表现不同。
- 有许多关于如何在 WPF 应用程序中显示或操作次显示器的主题。本文不包含这些链接,因为它们很容易通过 Google 搜索到。
原始帖子包含两台显示器的计算机截图。
特点
该应用程序展示了以下功能:
- 在所有显示器上输出最大化窗口
- 窗口包含一个列表框,其中显示了来自系统参数和
Screen
类的屏幕尺寸 - 依赖项容器的使用
背景
该解决方案使用了 C#6、.NET 4.6.1、带有 MVVM 模式的 WPF、System.Windows.Forms
、System.Drawing
以及 NuGet 包 Unity 和 Ikc5.TypeLibrary。
解决方案
该解决方案包含一个 WPF 应用程序项目。在 WPF 应用程序中,所有屏幕都被视为一个“虚拟”屏幕。为了定位窗口,有必要将窗口坐标设置在“当前”屏幕尺寸内。SystemParameters
提供了主屏幕的物理分辨率,但根据 WPF 的架构,元素的位置不应依赖于显示器的物理尺寸。主要问题是“当前”屏幕尺寸的确切值取决于多种因素。例如,它们包括 Windows 设置中的文本大小、用户如何连接到计算机(远程访问还是本地连接)等等。
System.Windows.Forms
库中的 Screen
类提供了有用的属性 Screen[] AllScreens
。它允许枚举所有屏幕并读取它们的工作区域。但非主屏幕的坐标是根据主窗口的“当前”文本大小设置的。
Application
由于窗口是手动创建的,因此应从 App.xaml 中删除 StartupUri
。OnStartup
方法执行以下代码:
- 初始化
Unity
容器 - 计算主屏幕的当前文本大小,作为
Screen.PrimaryScreen.WorkingArea
和SystemParameters.PrimaryScreen
之间的比率 - 为每个屏幕创建窗口,将其定位在当前屏幕上,显示并最大化
public partial class App : Application
{
private void InitContainer(IUnityContainer container)
{
container.RegisterType<ILogger, EmptyLogger>();
container.RegisterType<IMainWindowModel, MainWindowModel>();
}
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
IUnityContainer container = new UnityContainer();
InitContainer(container);
var logger = container.Resolve<ILogger>();
logger.Log($"There are {Screen.AllScreens.Length} screens");
// calculates text size in that main window (i.e. 100%, 125%,...)
var ratio =
Math.Max(Screen.PrimaryScreen.WorkingArea.Width /
SystemParameters.PrimaryScreenWidth,
Screen.PrimaryScreen.WorkingArea.Height /
SystemParameters.PrimaryScreenHeight);
var pos = 0;
foreach (var screen in Screen.AllScreens)
{
logger.Log(
$"#{pos + 1} screen, size = ({screen.WorkingArea.Left},
{screen.WorkingArea.Top}, {screen.WorkingArea.Width},
{screen.WorkingArea.Height}), " +
(screen.Primary ? "primary screen" : "secondary screen"));
// Show automata at all screen
var mainViewModel = container.Resolve<IMainWindowModel>(
new ParameterOverride("backgroundColor", _screenColors[Math.Min
(pos++, _screenColors.Length - 1)]),
new ParameterOverride("primary", screen.Primary),
new ParameterOverride("displayName", screen.DeviceName));
var window = new MainWindow(mainViewModel);
if (screen.Primary)
Current.MainWindow = window;
window.Left = screen.WorkingArea.Left / ratio;
window.Top = screen.WorkingArea.Top / ratio;
window.Width = screen.WorkingArea.Width / ratio;
window.Height = screen.WorkingArea.Height / ratio;
window.Show();
window.WindowState = WindowState.Maximized;
}
Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
}
private readonly Color[] _screenColors =
{
Colors.LightGray, Colors.DarkGray,
Colors.Gray, Colors.SlateGray, Colors.DarkSlateGray
};
}
MainWindow
主窗口包含 ListView
,它显示来自 MainWindowModel
视图模型的 ScreenRectangle
模型列表。为了在预期的屏幕上显示窗口,它应具有以下属性:
WindowStartupLocation="Manual"
WindowState="Normal"
此外,我们移除了标题栏并使窗口不可调整大小
WindowStyle="None"
ResizeMode="NoResize"
如果在 App.xaml.cs 中注释掉第 45 行
window.WindowState = WindowState.Maximized;
那么窗口将占据除任务栏以外的整个屏幕。
MainWindowModel
MainWindowModel
类实现了 IMainWindowModel
接口
public interface IMainWindowModel
{
/// <summary>
/// Background color.
/// </summary>
Color BackgroundColor { get; }
/// <summary>
/// Width of the view.
/// </summary>
double ViewWidth { get; set; }
/// <summary>
/// Height of the view.
/// </summary>
double ViewHeight { get; set; }
/// <summary>
/// Set of rectangles.
/// </summary>
ObservableCollection<ScreenRectangle> Rectangles { get; }
}
背景颜色用于为窗口着色,并在不同屏幕上区分它们。ViewHeight
和 ViewWidth
绑定到附加属性,以便在视图模型中获取视图大小(代码来自 Pushing read-only GUI properties back into ViewModel)。ScreenRectangle
类看起来像 Tuple<string, Rectangle>
的派生类,实现了 NotifyPropertyChanged
接口。
public class ScreenRectangle : BaseNotifyPropertyChanged
{
protected ScreenRectangle()
{
Name = string.Empty;
Bounds = new RectangleF();
}
public ScreenRectangle(string name, RectangleF bounds)
{
Name = name;
Bounds = bounds;
}
public ScreenRectangle(string name, double left, double top, double width, double height)
: this(name, new RectangleF((float)left, (float)top, (float)width, (float)height))
{
}
public ScreenRectangle(string name, double width, double height)
: this(name, new RectangleF(0, 0, (float)width, (float)height))
{
}
#region Public properties
private string _name;
private RectangleF _bounds;
public string Name
{
get { return _name; }
set { SetProperty(ref _name, value); }
}
public RectangleF Bounds
{
get { return _bounds; }
set { SetProperty(ref _bounds, value); }
}
#endregion Public properties
public void SetSize(double width, double height)
{
Bounds = new RectangleF(Bounds.Location, new SizeF((float)width, (float)height));
}
}
更新 1
示例应用程序已更新。现在它显示更多的系统参数,并且一些尺寸已注释掉。
主要更改在 MainWindowModel
类的构造函数中。
public MainWindowModel(Color backgroundColor, bool primary, string displayName)
{
this.SetDefaultValues();
BackgroundColor = backgroundColor;
_rectangles = new ObservableCollection<screenrectangle>(new[]
{
new ScreenRectangle(ScreenNames.View, ViewWidth, ViewHeight,
"View uses border with BorderThickness='3',
and its size is lesser than screen size at 6px at each dimension")
});
if( primary)
{
_rectangles.Add(new ScreenRectangle(ScreenNames.PrimaryScreen,
(float)SystemParameters.PrimaryScreenWidth,
(float)SystemParameters.PrimaryScreenHeight,
"'Emulated' screen dimensions for Wpf applications"));
_rectangles.Add(new ScreenRectangle(ScreenNames.FullPrimaryScreen,
(float) SystemParameters.FullPrimaryScreenWidth,
(float) SystemParameters.FullPrimaryScreenHeight,
"Height difference with working area height depends on locale"));
_rectangles.Add(new ScreenRectangle(ScreenNames.VirtualScreen,
(float) SystemParameters.VirtualScreenLeft,
(float) SystemParameters.VirtualScreenTop,
(float) SystemParameters.VirtualScreenWidth,
(float) SystemParameters.VirtualScreenHeight));
_rectangles.Add(new ScreenRectangle(ScreenNames.WorkingArea,
SystemParameters.WorkArea.Width, SystemParameters.WorkArea.Height,
"40px is holded for the taskbar height"));
_rectangles.Add(new ScreenRectangle(ScreenNames.PrimaryWorkingArea,
Screen.PrimaryScreen.WorkingArea.Left, Screen.PrimaryScreen.WorkingArea.Top,
Screen.PrimaryScreen.WorkingArea.Width,
Screen.PrimaryScreen.WorkingArea.Height));
}
foreach (var screeen in Screen.AllScreens)
{
if (!primary && !Equals(screeen.DeviceName, displayName))
continue;
_rectangles.Add(new ScreenRectangle($"Screen \"{screeen.DeviceName}\"",
screeen.WorkingArea,
"Physical dimensions"));
}
}
更新 2
最近,我们遇到一个问题:在 Windows 7/10 英文版中,WPF 应用程序中的固定大小对话框工作正常,但当用户使用 Windows 7 中文版时,一些控件会被截断或不显示。对于 WinForm 应用程序有解决方案,但它不适用于 WPF 应用程序,因为它们使用不同的缩放方法。此问题通过两个步骤得到解决:
- 固定大小被替换为通过
UserControl.Measuze()
方法计算的期望大小。 - 注意到主屏幕的总尺寸小于工作区域尺寸,但差异取决于区域设置。令
localeDelta = SystemParameters.WorkArea.Height - SystemParameters.FullPrimaryScreenHeight
。那么:- 对于 Windows 7/10 英文区域设置,
localeDelta == 13.14
,它对屏幕尺寸略有影响。 - 对于 Windows 7 中文区域设置,
localeDelta == 22
。
localeDelta
就足够了。 - 对于 Windows 7/10 英文区域设置,
历史
- 2017 年 1 月 14 日:初次发布
- 2017 年 5 月 23 日:向窗口添加了工作区域大小
- 2017 年 7 月 2 日:添加了显示尺寸的注释,更新了 Git 存储库