WPF桌面端开发1-窗口设置及生命周期

窗口是否可见

判断是否可见

通过Visibility判断

1
2
3
4
if (Visibility == Visibility.Hidden)
{
return;
}

如果判断特定状态使用Visibility比较好。

通过IsVisible判断

但要注意,窗口的 IsVisible 属性会在以下情况下变为 false

  1. 窗口被隐藏:当调用窗口的 Hide() 方法,或者将窗口的 Visibility 属性设置为 Collapsed 时,窗口会被隐藏,IsVisible 属性值会变为 false

  2. 窗口被最小化:当将窗口的 WindowState 属性设置为 WindowState.Minimized 或者通过系统的最小化按钮将窗口最小化时,窗口会被最小化并隐藏,IsVisible 属性值会变为 false

  3. 窗口被关闭:当调用窗口的 Close() 方法关闭窗口时,窗口会被关闭并隐藏,IsVisible 属性值会变为 false

需要注意的是,这些操作会直接改变窗口的可见性。

但是,窗口的可见性也可能受到其他因素的影响,例如被其他窗口遮挡而无法显示在屏幕上,此时 IsVisible 仍然为 true

因此,根据具体的需求,可能需要综合考虑其他因素来判断窗口是否可见。

窗口激活

有时窗口显示的时候,但是被其它窗口遮挡,我们想让他激活到前台。

1
2
3
4
5
6
7
if (quesResultsWindow is { Visibility: Visibility.Visible })
{
quesResultsWindow?.WindowState = WindowState.Normal;
quesResultsWindow?.Focus();
quesResultsWindow?.Activate();
return;
}

其中

  • Focus 会获取焦点,不会激活前台。
  • Activate 只会激活前台,不会获取焦点。

监听状态变化

在 WPF 中,可以通过绑定窗口的 Visibility 属性并实现可视化状态变化事件的处理程序,来监听窗口可视化状态的变化。具体来说,可以使用 FrameworkElementVisibilityChanged 事件,该事件会在元素的 Visibility 属性值更改时被触发。在窗口中,可以将其绑定到窗口对象的 Visibility 属性,并在窗口可视化状态更改时执行相应的处理程序。

以下是一个在 WPF 窗口中使用 VisibilityChanged 事件的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;

// 绑定 VisibilityChanged 事件
DependencyPropertyDescriptor.FromProperty(VisibilityProperty, typeof(UIElement)).AddValueChanged(this, OnVisibilityChanged);
}

private void OnVisibilityChanged(object sender, EventArgs e)
{
if (Visibility == Visibility.Hidden)
{
// 窗口被隐藏
}
else if (Visibility == Visibility.Visible)
{
// 窗口可见
}
}
}

在上面的示例中,我们将窗口的可视化状态绑定到了 VisibilityChanged 事件,并在事件处理程序中进行了相应的处理。

需要注意的是,对于较复杂的应用,通常使用 MVVM 模式来进行开发,将窗口的可视化状态绑定到 ViewModel 中的属性,并在属性更改时触发相应的事件,以便在窗口与 ViewModel 之间进行数据绑定。

窗体拖拽

无边框情况下默认是无法拖拽的,如果需要拖拽则为Window的MouseLeftButtonDown绑定事件,并调用默认DragMove方法即可。

XAML:

1
MouseLeftButtonDown="WindowMouseLeftButtonDown"

C#:

1
2
3
4
5
6
7
private void WindowMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
DragMove();
}
}

注意

一定不要在外层和内层都添加该方法,会导致拖拽时崩溃。

透明

1
2
3
4
5
<Window
AllowsTransparency="True"
WindowStyle="None"
Background="Transparent">
</Window>

窗口不透明时去掉顶部白边

1
2
3
4
5
6
<Window
AllowsTransparency="False"
Background="White"
ResizeMode="NoResize"
WindowStyle="None">
</Window>

主要是ResizeMode属性

不在状态栏显示

1
ShowInTaskbar="False"

居中显示

1
WindowStartupLocation="CenterScreen"

窗口阴影

设置WindowChrome(推荐)

不显示窗口的按钮,但是保留窗口的阴影

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<Window.Style>
<Style TargetType="Window">
<Setter Property="AllowsTransparency" Value="False" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="WindowChrome.WindowChrome">
<Setter.Value>
<WindowChrome
CaptionHeight="0"
CornerRadius="0"
GlassFrameThickness="-1"
NonClientFrameEdges="None"
ResizeBorderThickness="0"
UseAeroCaptionButtons="False" />
</Setter.Value>
</Setter>

<Style.Triggers>
<Trigger Property="WindowState" Value="Maximized">
<Setter Property="BorderThickness" Value="6" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Style>

注意

这里添加了一个触发器,当窗口最大化的时候设置了边框宽度是6,这是因为虽然我们设置BorderThickness为0,但是最大化的时候,框架傻傻的还是减去了默认值6,会导致窗口最大化时显示不全,所以这时候设置触发器,就正好解决了这个问题。

WindowChrome 是 WPF 4.5 中的新特性之一,它允许您完全自定义 Windows 窗口的样式,包括标题栏、窗口边框和窗口阴影等。WindowChrome 类包含一组属性,用于定义窗口的样式和行为。

以下是 WindowChrome 类的主要属性:

  • CaptionHeight:窗口标题栏的高度。默认值为 30 像素。
  • CornerRadius:窗口边框和标题栏的圆角半径。默认值为 0。
  • GlassFrameThickness:在使用 Aero 效果时,控制窗口边框的渐变区域大小。默认值为 1 像素。当为-1的时候使用窗口默认样式。
  • ResizeBorderThickness:控制窗口边框的大小,以便用户可以通过窗口边缘或角来调整窗口大小。默认值为 6 像素。
  • UseAeroCaptionButtons:指示是否应使用 Windows Aero 形式的标题栏按钮。默认值为 true。
  • WindowChrome.IsHitTestVisibleInChrome:指示是否应将鼠标单击事件路由到窗口内容。默认值为 false。
  • WindowChrome.IsHitTestVisibleInNonClientArea:指示是否应将鼠标单击事件路由到窗口的非客户区域。默认值为 false。

要显示窗口的阴影要设置一下的值

  • AllowsTransparency="False"不能透明,透明就没阴影了。
  • WindowStyle="SingleBorderWindow"不能设置为 WindowStyle="None",就是我们要保留默认的样式,这样才能显示阴影。
  • GlassFrameThickness="-1",当该值为-1的时候,不会CornerRadius设置圆角的值不生效。

总的来说

保留窗口的默认样式,把标题栏高度设置为0。

为了页面公用我们也可以定义样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<Style x:Key="ZWinStyle" TargetType="Window">
<Setter Property="AllowsTransparency" Value="False" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="WindowChrome.WindowChrome">
<Setter.Value>
<WindowChrome
CaptionHeight="0"
CornerRadius="0"
GlassFrameThickness="-1"
NonClientFrameEdges="None"
ResizeBorderThickness="0"
UseAeroCaptionButtons="False" />
</Setter.Value>
</Setter>

<Style.Triggers>
<Trigger Property="WindowState" Value="Maximized">
<Setter Property="BorderThickness" Value="6" />
</Trigger>
</Style.Triggers>
</Style>

使用

1
2
3
4
<Window
Style="{StaticResource ZWinStyle}"
WindowStyle="SingleBorderWindow">
</Window>

窗口默认样式

也就是窗口不是WindowStyle="None"的情况下,但是我们为了自定义窗口上的按钮,都不会使用这种方式。

1
2
3
4
5
6
7
<Window.Effect>
<DropShadowEffect
BlurRadius="10"
Direction="80"
ShadowDepth="0"
Color="#f3f3f3" />
</Window.Effect>

窗口透明

示例

这种方式在最大化的时候,因为阴影宽度的问题会导致四周没靠边。

首先添加窗口透明

1
2
3
4
5
6
<Window
AllowTransparency="True"
WindowStyle="None"
Background="Transparent"
ResizeMode="NoResize">
</Window>

添加全局样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Style x:Key="ShadowStyle" TargetType="Border">
<Setter Property="Margin" Value="8" />
<Setter Property="Background" Value="White" />
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect
BlurRadius="8"
Direction="270"
Opacity="0.2"
RenderingBias="Quality"
ShadowDepth="2"
Color="Black" />
</Setter.Value>
</Setter>
</Style>

使用

1
2
<Border Style="{StaticResource ShadowStyle}">
</Border>

注意

阴影区域,也就是Margin的值要是4的倍数,否则界面会模糊。

参数介绍

WPF中的DropShadowEffect提供了设置窗口阴影的功能。

下面是它的参数介绍:

  1. BlurRadius:模糊半径,用来控制阴影的边缘模糊程度。
  2. Color:阴影的颜色。
  3. Direction:阴影的方向,用一个度数来表示。
  4. Opacity:阴影的不透明度,控制阴影的透明度程度。
  5. ShadowDepth:阴影的深度,表示阴影相对于元素的距离。
  6. RenderingBias:阴影的呈现方式,用于指定是以速度优先还是质量优先进行呈现。

可以根据需要对以上参数进行调整,以获取更好的阴影效果。

窗口界面模糊

先说结论

像素最好是4的倍数

原因

假如我们电脑的分辨率为1920*1080,缩放为125%,那么WPF最大窗口尺寸为1536*864。也就是都要除以1.25

那么我们窗口的宽、高、Margin和Padding都要乘以1.25才是实际的像素。

只有实际像素为整数时才能显示清晰。

假如我们设置的Margin是10的话,实际像素就是12.5,就会导致页面模糊。

为什么是4的倍数?

我们常见的缩放为100%、125%、150%、175%、200%,这些乘以4都是整数。

最小化/最大化

最小化

1
2
3
4
private void MiniBtn_Click(object sender, RoutedEventArgs e)
{
WindowState = WindowState.Minimized;
}

最大化

1
2
3
4
private void MaxBtn_Click(object sender, RoutedEventArgs e)
{
WindowState = WindowState.Maximized;
}

XML中

1
WindowState="Maximized"

居中显示

1
WindowStartupLocation="CenterOwner"

获取屏幕的宽高

主显示器的宽高

这个值是实际的像素除以缩放后的值

1
2
readonly double _screenWidth = SystemParameters.PrimaryScreenWidth;
readonly double _screenHeight = SystemParameters.PrimaryScreenHeight;

宽高(实际的像素)

1
2
int screenWidth = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width;
int screenHeight = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height;

获取系统缩放

1
2
3
4
int screenWidth = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width;
double screenWidthScale = SystemParameters.PrimaryScreenWidth;
double ratio = screenWidth / screenWidthScale;
Console.WriteLine($@"系统缩放{ratio}");

获取窗口像素位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private double _left;
private double _right;
private double _top;
private double _bottom;

private void GetWinPos()
{
int screenWidth = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width;
double screenWidthScale = SystemParameters.PrimaryScreenWidth;
double ratio = screenWidth / screenWidthScale;
_left = ratio * Left;
_right = ratio * (Left + Width);
_top = ratio * Top;
_bottom = ratio * (Top + Height);
Console.WriteLine($@"窗口的像素位置:left:{_left} right:{_right} top:{_top} bottom:{_bottom}");
}

Enter登录

在登录界面我们想使用Enter触发登录的事件

可以在登录按钮上添加

1
IsDefault="True"

并且保证别的按钮没有添加这个属性,这样就实现了点击Enter登录的操作。

生命周期

从前到后以此为:

Constructor

Initialized

SourceInitialized 显示窗口,事件发生后获取窗体的句柄,并且可注册全局快捷键。

Activated 活动窗口

Loaded 所有内容都显示给用户之前执行,窗口渲染完成,但是还没有执行任何交互时触发,在程序加载期间做一些初始化操作

Deactivated 多个窗口中切换时,Activated和Deactivated在窗口的生命周期里会发生多次

ContentRendered 窗口第一次完全呈现出来时触发,说明窗口已打开

SourceUpdated用户和窗口进行交互

Closing

Closed

Unloaded

按钮事件

在 WPF 中,为按钮添加事件和解除事件最好在 Loaded 和 Unloaded 生命周期中进行。

Loaded 表示窗口已加载完成,此时所有元素和资源均已初始化完毕,可以安全地添加事件处理程序。

Unloaded 表示窗口即将从内存中卸载,此时应解除所有事件处理程序,以避免资源泄漏。

新线程长期任务

如果我们要在页面启动的时候,同时执行另一个长期执行的线程,这时候如果页面关闭,线程不会关闭,就会造成内存泄漏。

建议添加一个变量在页面关闭事件中设置变量为false,线程也就停止了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private bool _isStart;
public MainWindow()
{
InitializeComponent();
Closing += MainWindow_Closing;
new Thread
(
() =>
{
while (_isStart)
{

}
}
).Start();
}

private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
_isStart = false;
}

如果我们还要操作UI线程的元素的时候,一定也要判断一下变量的值,防止页面已销毁而导致崩溃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (_isStart)
{
Dispatcher.Invoke
(
() =>
{
var bmp = decoder.Decode(data, length);
if (bmp != null)
{
Console.WriteLine(bmp.Size);
var bitmapImage = ZImageUtils.Bitmap2BitmapImage(bmp);
MImg.Source = bitmapImage;
bmp.Dispose();
}
}
);
}

Owner

在 WPF 中,设置窗口的 Owner(拥有者)有以下作用:

创建模态对话框

创建模态对话框:

使用 Owner 属性可以将一个窗口设置为另一个窗口的所有者。

这意味着拥有者窗口会成为被拥有窗口的模态对话框的所有者。

被拥有窗口将保持在拥有者窗口之上,用户无法与其他窗口进行交互,直到关闭拥有者窗口或者调用被拥有窗口的 Close 方法。

这对于实现需要顺序进行的工作流程或者确保用户完成某个任务非常有用。

示例代码:

1
2
3
4
MainWindow ownerWindow = new MainWindow();
ChildWindow childWindow = new ChildWindow();
childWindow.Owner = ownerWindow;
childWindow.ShowDialog(); // 显示模态对话框,ownerWindow 成为其所有者

实现窗口层次结构

实现窗口层次结构:

设置窗口的 Owner 属性可以创建一个窗口层次结构,其中子窗口与父窗口相关联。

当父窗口最小化、最大化或者关闭时,相关联的子窗口也会进行相应的处理,以确保用户体验的连续性。

示例代码:

1
2
3
4
5
6
MainWindow parentWindow = new MainWindow();
ChildWindow childWindow1 = new ChildWindow();
ChildWindow childWindow2 = new ChildWindow();
childWindow1.Owner = parentWindow;
childWindow2.Owner = parentWindow;
parentWindow.Show();

在此示例中,当 parentWindow 最小化、最大化或者关闭时,childWindow1 和 childWindow2 也会相应地进行处理。

实现窗口间的数据传递

实现窗口间的数据传递:

通过 Owner 属性,你可以在窗口之间传递数据。

拥有者窗口可以提供对必要数据的访问,以便子窗口使用或更新这些数据。

示例代码:

在拥有者窗口中:

1
2
3
ChildWindow childWindow = new ChildWindow();
childWindow.Owner = this;
childWindow.ShowDialog(); // 显示子窗口,并将数据传递给子窗口

在子窗口中:

1
2
MainWindow ownerWindow = (MainWindow)this.Owner;
// 访问或更新拥有者窗口中的数据

通过 Owner 属性,子窗口可以访问拥有者窗口中的数据并进行操作。

总结:

设置窗口的 Owner 属性可以创建模态对话框、实现窗口层次结构和实现窗口间的数据传递。

同时,使用 Owner 属性可以提升用户体验和界面交互的一致性。