WPF 样式与线程服务深度解析
1. WPF 样式基础
WPF 的控件高度依赖样式和模板,其设计理念是控件本身不包含硬编码的属性值、默认值或视觉效果,以便图形设计师和主题作者能完全控制控件的默认设置。在样式中,我们通常只能调整少数属性,那么其余属性值从何而来呢?
WPF 中每个属性都有值优先级的概念,所有功能(如样式设置、数据绑定、继承等)都按严格顺序应用。实际上,每个元素会应用两种样式,不过使用控件时,通常只能看到一种,因为另一种是为控件作者保留的。在所有情况下,本地属性值会优先于样式指定的值。
除了基本的优先级,我们还可以创建一个基础样式,让多个样式继承它。样式只能有一个父样式,但一个基础样式可以应用于多种子样式。
以下是创建基础样式的示例代码:
<Style x:Key='baseControls' TargetType='{x:Type Control}'>
<Setter Property='FontSize' Value='14pt' />
<Setter Property='FontFamily' Value='Corbel, Arial' />
<Setter Property='Margin' Value='2' />
</Style>
然后,我们可以使用正常的样式定义并设置
BasedOn
属性,将基础样式与多个控件类型关联起来:
<Style
x:Key='{x:Type TextBox}'
TargetType='{x:Type TextBox}'
BasedOn='{StaticResource baseControls}' />
为了更直观地展示其强大功能,我们可以为不同的控件类型创建多个样式。例如,我们希望按钮无论使用何种基础字体都显示为粗体:
<Window x:Class='Styles.StyleInheritence'
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Title='Styles'
>
<Window.Resources>
<Style x:Key='baseControls' TargetType='{x:Type Control}'>
<Setter Property='FontSize' Value='14pt' />
<Setter Property='FontFamily' Value='Corbel, Arial' />
<Setter Property='Margin' Value='2' />
</Style>
<Style
x:Key='{x:Type Button}'
TargetType='{x:Type Button}'
BasedOn='{StaticResource baseControls}'>
<Setter Property='FontWeight' Value='Bold' />
</Style>
<Style
x:Key='{x:Type CheckBox}'
TargetType='{x:Type CheckBox}'
BasedOn='{StaticResource baseControls}' />
<Style
x:Key='{x:Type TextBox}'
TargetType='{x:Type TextBox}'
BasedOn='{StaticResource baseControls}' />
</Window.Resources>
<StackPanel>
<CheckBox>Hello World</CheckBox>
<TextBox>Hello World</TextBox>
<Button>Hello World</Button>
<Button>Hello World</Button>
</StackPanel>
</Window>
运行这段代码后,我们可以看到三种控件类型都从
baseControls
样式中获取字体家族和大小,并且所有按钮都显示为粗体。
2. 合理使用样式的原则
样式的引入以及 WPF 的完全可定制性是相对于以往呈现技术的重大变革。人们定义自定义样式主要有三个原因:
- 跨两个或多个实例共享属性集。
- 将所有自定义设置集中到应用程序的一个点。
- 为应用程序定义独特的外观。
前两个用途是非常好的做法,合理使用样式可以显著提高开发效率。但对于定义应用程序自定义外观的样式,我们需要注意以下三点:
-
构建主题而非仅样式
:自定义主题通常会变得很庞大,可能涉及大量的标记代码以及模板、样式和资源之间的依赖关系。因此,最佳实践是将主题定义移到单独的文件中,并使用合并字典功能将主题绑定到应用程序中。甚至可以将主题定义移到单独的程序集中,使用跨程序集引用加载样式。示例代码如下:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source='/StyleLibrary;Component/theme.xaml' />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
这种做法可以将主题与应用程序分离,便于将主题交给设计师处理,并促使我们将主题视为一个独立的组件。
-
一致性至上 :构建应用程序的自定义外观时,确保所有使用的控件保持一致非常重要。WPF 的目标之一是使每个控件都可定制,避免出现类似旧窗体包中那种“拼凑”的 UI。创建一致设计的最简单方法是选择一个主题(如“金属风”“扁平风”等)。如果样式在外观和控件覆盖范围上都保持一致,还需要有一个元素将整个主题联系起来。
-
主题要有意义 :自定义主题可以使应用程序与众不同,但创建自定义主题在设计和实现方面都可能成本较高。关键是自定义主题要向用户传达某种信息,例如公司品牌或应用程序的“酷炫”风格。我们应该能够用一句话概括这个信息。对于实用程序或标准业务应用程序,自定义主题可能不会增加太多价值,但在某些情况下,使用主题来强制执行良好的可用性准则可能会很有趣。
3. WPF 线程与调度器
最初,WPF 开发团队的目标之一是摆脱对单线程单元(STA)线程模型的依赖。然而,由于许多 ActiveX 控件和其他基于 COM 的服务需要 STA 线程模型,为了与现有的大多数服务兼容,WPF 最终还是采用了 STA 线程模型。
在编写典型的 Hello World 应用程序时,我们会在入口点函数上看到
STAThread
属性:
using System;
using System.Windows;
class Program {
[STAThread]
static void Main() {
Application app = new Application();
Window w = new Window();
w.Show();
app.Run();
}
}
大多数 WPF 对象都派生自
DispatcherObject
,我们将对象与单个调度器关联,而不是与线程关联。调度器负责接收消息并将其分发到正确的对象。与调度器和 WPF 线程模型相关的大多数类型都位于
System.Windows.Threading
命名空间中。
真正的共享内存并发(多线程)在当今的主流编程语言中很难正确编程,因为涉及复杂的锁定和并发管理规则。最成功的多任务处理模型是通过异步消息传递模型在两个任务之间实现松散耦合。
.NET Framework 2.0 引入了
SynchronizationContext
类,WPF 利用它将调度器的引用推送到上下文中,使得组件(如
System.Net.WebClient
)的异步回调可以自动发布回 UI 线程。
以下是一个使用
WebClient
异步下载 HTML 页面的示例:
<Window x:Class='ThreadingExample.Window1'
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Title='ThreadingExample'
>
<StackPanel>
<TextBox x:Name='_text'
Height='150'
HorizontalScrollBarVisibility='Auto'
VerticalScrollBarVisibility='Auto' />
<Button Click='DownloadNormally'>Download</Button>
</StackPanel>
</Window>
using System;
namespace ThreadingExample {
public partial class Window1 : Window {
public Window1() {
InitializeComponent();
}
void DownloadNormally(object sender, RoutedEventArgs e) {
WebClient wc = new WebClient();
wc.DownloadStringCompleted +=
delegate(object sender2,
DownloadStringCompletedEventArgs e2)
{
_text.Text = e2.Result;
};
wc.DownloadStringAsync(new Uri("http://www.msn.com"));
}
}
}
当需要显式进行多线程处理时,我们有两种方式与 UI 线程通信:
- 如果创建的是可在 WPF 应用程序之外使用的可重用组件,应像
WebClient
一样使用
SynchronizationContext
管理回调。
- 如果实现的是与特定程序相关的应用程序逻辑,可以直接使用与 UI 线程关联的调度器。直接与调度器通信可以更好地控制消息处理,但在 Windows Forms 或 ASP.NET 中无法将回调发布到正确的上下文。
以下是一个计算从 0 到
(n - 1)
数字之和的示例:
class SumCompletedEventArgs : EventArgs {
int _result;
public int Result {
get { return _result;}
set { _result = value;}
}
}
class Sum {
SynchronizationContext _context;
public Sum() {
_context = SynchronizationContext.Current;
}
public event EventHandler<SumCompletedEventArgs> SumCompleted;
public void SumAsync(int value) {
Thread background = new Thread(BackgroundTask);
background.IsBackground = true;
background.Start(value);
}
void BackgroundTask(object parameter) {
int value = (int)parameter;
int result = 0;
for (int i = 0; i <= value; i++) {
result += i;
Thread.Sleep(10);
}
_context.Post(Completed, result);
}
void Completed(object result) {
if (SumCompleted != null) {
SumCompletedEventArgs e = new SumCompletedEventArgs();
e.Result = (int)result;
SumCompleted(this, e);
}
}
}
使用示例:
void RunTask(object sender, RoutedEventArgs e) {
Sum t = new Sum();
t.SumCompleted +=
delegate(object sender2, SumCompletedEventArgs e2) {
_text.Text = e2.Result.ToString();
};
t.SumAsync(250);
}
通过以上内容,我们深入了解了 WPF 样式的工作原理、合理使用样式的原则以及 WPF 线程和调度器的相关知识。这些知识对于开发高质量的 WPF 应用程序至关重要。
WPF 样式与线程服务深度解析(续)
4. 线程处理示例分析
在前面提到的线程处理示例中,我们详细探讨了使用
WebClient
进行异步下载以及自定义任务计算数字之和的情况。下面我们进一步分析这些示例的关键步骤和要点。
4.1
WebClient
异步下载示例
这个示例展示了如何使用
WebClient
类异步下载 HTML 内容。以下是其主要步骤:
1.
创建
WebClient
实例
:在
DownloadNormally
方法中,我们创建了一个
WebClient
对象。
2.
注册完成事件处理程序
:使用
DownloadStringCompleted
事件,当下载完成时,将下载的内容显示在
TextBox
中。
3.
启动异步下载
:调用
DownloadStringAsync
方法,并传入要下载的 URL。
<Window x:Class='ThreadingExample.Window1'
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Title='ThreadingExample'
>
<StackPanel>
<TextBox x:Name='_text'
Height='150'
HorizontalScrollBarVisibility='Auto'
VerticalScrollBarVisibility='Auto' />
<Button Click='DownloadNormally'>Download</Button>
</StackPanel>
</Window>
using System;
namespace ThreadingExample {
public partial class Window1 : Window {
public Window1() {
InitializeComponent();
}
void DownloadNormally(object sender, RoutedEventArgs e) {
WebClient wc = new WebClient();
wc.DownloadStringCompleted +=
delegate(object sender2,
DownloadStringCompletedEventArgs e2)
{
_text.Text = e2.Result;
};
wc.DownloadStringAsync(new Uri("http://www.msn.com"));
}
}
}
这个示例的优点在于,
WebClient
会自动使用
SynchronizationContext
将回调发布到 UI 线程,我们无需手动处理线程同步问题。
4.2 自定义任务计算数字之和示例
这个示例展示了如何创建一个自定义任务来计算从 0 到
(n - 1)
的数字之和,并在后台线程中执行该任务。以下是其主要步骤:
1.
定义结果事件参数类
:
SumCompletedEventArgs
类用于存储计算结果。
2.
创建任务类
:
Sum
类包含一个
SynchronizationContext
实例,用于管理回调。
3.
实现异步方法
:
SumAsync
方法创建一个后台线程并启动任务。
4.
实现后台任务
:
BackgroundTask
方法在后台线程中执行计算,并使用
SynchronizationContext
将结果发送回 UI 线程。
5.
处理完成事件
:
Completed
方法在 UI 线程中处理计算结果,并触发
SumCompleted
事件。
class SumCompletedEventArgs : EventArgs {
int _result;
public int Result {
get { return _result;}
set { _result = value;}
}
}
class Sum {
SynchronizationContext _context;
public Sum() {
_context = SynchronizationContext.Current;
}
public event EventHandler<SumCompletedEventArgs> SumCompleted;
public void SumAsync(int value) {
Thread background = new Thread(BackgroundTask);
background.IsBackground = true;
background.Start(value);
}
void BackgroundTask(object parameter) {
int value = (int)parameter;
int result = 0;
for (int i = 0; i <= value; i++) {
result += i;
Thread.Sleep(10);
}
_context.Post(Completed, result);
}
void Completed(object result) {
if (SumCompleted != null) {
SumCompletedEventArgs e = new SumCompletedEventArgs();
e.Result = (int)result;
SumCompleted(this, e);
}
}
}
使用示例:
void RunTask(object sender, RoutedEventArgs e) {
Sum t = new Sum();
t.SumCompleted +=
delegate(object sender2, SumCompletedEventArgs e2) {
_text.Text = e2.Result.ToString();
};
t.SumAsync(250);
}
这个示例展示了如何手动管理异步任务和回调,适用于需要更复杂线程处理的场景。
5. 样式与线程服务的综合应用
在实际的 WPF 应用程序中,样式和线程服务通常会结合使用。例如,我们可以在异步任务完成后更新 UI 元素的样式。以下是一个简单的示例流程图,展示了如何在异步任务完成后更新按钮的样式:
graph TD;
A[启动异步任务] --> B[后台线程执行任务];
B --> C{任务完成?};
C -- 是 --> D[使用调度器更新 UI];
D --> E[更新按钮样式];
C -- 否 --> B;
在代码实现上,我们可以在
SumCompleted
事件处理程序中更新按钮的样式。假设我们有一个按钮
myButton
,并且已经定义了一个名为
NewStyle
的样式:
<Window.Resources>
<Style x:Key="NewStyle" TargetType="Button">
<Setter Property="Background" Value="Red"/>
<Setter Property="Foreground" Value="White"/>
</Style>
</Window.Resources>
<Button x:Name="myButton" Content="Click me"/>
void RunTask(object sender, RoutedEventArgs e) {
Sum t = new Sum();
t.SumCompleted +=
delegate(object sender2, SumCompletedEventArgs e2) {
_text.Text = e2.Result.ToString();
myButton.Style = (Style)FindResource("NewStyle");
};
t.SumAsync(250);
}
6. 总结与最佳实践
通过对 WPF 样式和线程服务的深入探讨,我们可以总结出以下最佳实践:
6.1 样式方面
- 使用基础样式 :创建基础样式可以减少重复代码,提高代码的可维护性。例如,我们可以创建一个包含常用字体和边距设置的基础样式,然后让其他样式继承它。
- 合理定义主题 :将主题定义移到单独的文件或程序集中,便于管理和维护。同时,确保主题在外观和控件覆盖范围上保持一致,并且有明确的设计意图。
- 遵循优先级规则 :了解 WPF 属性值的优先级规则,确保本地属性值和样式设置不会冲突。
6.2 线程方面
-
优先使用异步编程
:使用异步方法(如
WebClient的异步下载方法)可以避免阻塞 UI 线程,提高用户体验。 -
正确处理回调
:使用
SynchronizationContext或调度器确保异步回调在正确的线程中执行。 - 避免线程安全问题 :在多线程编程中,要注意避免数据竞争和死锁等问题。例如,不要在后台线程中直接访问 UI 元素,而是通过回调将结果传递给 UI 线程处理。
通过遵循这些最佳实践,我们可以开发出更加高效、稳定和美观的 WPF 应用程序。在实际开发中,我们需要根据具体需求灵活运用样式和线程服务,不断优化代码,提升应用程序的质量。
超级会员免费看

370

被折叠的 条评论
为什么被折叠?



