65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 双屏显示

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (8投票s)

2017年1月13日

CPOL

4分钟阅读

viewsIcon

39163

downloadIcon

2077

将 WPF 窗口定位在第二个显示器上。

引言

本文展示了如何在次显示器上定位 WPF 窗口或在两台显示器上显示两个窗口。本文包含完整的代码,并讨论了如何处理几种场景。

有以下免责声明:

  1. 我提供的代码和结论基于经验结果,因此我可能出错或描述不准确。
  2. 代码在具有两台显示器的计算机上进行了测试,因此三台或更多显示器可能表现不同。
  3. 有许多关于如何在 WPF 应用程序中显示或操作次显示器的主题。本文不包含这些链接,因为它们很容易通过 Google 搜索到。

原始帖子包含两台显示器的计算机截图。

特点

该应用程序展示了以下功能:

  1. 在所有显示器上输出最大化窗口
  2. 窗口包含一个列表框,其中显示了来自系统参数和 Screen 类的屏幕尺寸
  3. 依赖项容器的使用

背景

该解决方案使用了 C#6、.NET 4.6.1、带有 MVVM 模式的 WPF、System.Windows.FormsSystem.Drawing 以及 NuGet 包 UnityIkc5.TypeLibrary

解决方案

该解决方案包含一个 WPF 应用程序项目。在 WPF 应用程序中,所有屏幕都被视为一个“虚拟”屏幕。为了定位窗口,有必要将窗口坐标设置在“当前”屏幕尺寸内。SystemParameters 提供了主屏幕的物理分辨率,但根据 WPF 的架构,元素的位置不应依赖于显示器的物理尺寸。主要问题是“当前”屏幕尺寸的确切值取决于多种因素。例如,它们包括 Windows 设置中的文本大小、用户如何连接到计算机(远程访问还是本地连接)等等。

System.Windows.Forms 库中的 Screen 类提供了有用的属性 Screen[] AllScreens。它允许枚举所有屏幕并读取它们的工作区域。但非主屏幕的坐标是根据主窗口的“当前”文本大小设置的。

Application

由于窗口是手动创建的,因此应从 App.xaml 中删除 StartupUriOnStartup 方法执行以下代码:

  • 初始化 Unity 容器
  • 计算主屏幕的当前文本大小,作为 Screen.PrimaryScreen.WorkingAreaSystemParameters.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; }
}

背景颜色用于为窗口着色,并在不同屏幕上区分它们。ViewHeightViewWidth 绑定到附加属性,以便在视图模型中获取视图大小(代码来自 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 就足够了。

历史

  • 2017 年 1 月 14 日:初次发布
  • 2017 年 5 月 23 日:向窗口添加了工作区域大小
  • 2017 年 7 月 2 日:添加了显示尺寸的注释,更新了 Git 存储库
© . All rights reserved.