# 前言
之前本来是跟 开发一个批量图片处理工具 写一块的,用于记录使用 WPF 开发那个工具时,过程中碰到的各种问题,后来发现越记录越多,还是新开一个文档记录了。
# 问题记录
说实话,UI 问题反而是最多的
# 枚举文件
使用 Directory.EnumerateFiles
比 Directory.GetFiles
更好,包括内存及可开始处理的时间,因为前者通过迭代器返回,后者必须一次性全部搜集完成才能返回值。
例如目录下有 1000 个文件,我们实际上只需要取其中一个,枚举的方式就可以在取得之后直接返回,避免多余的消耗。
更多测试结果见:Batch-Processing-with-Directory-EnumerateFiles
需要注意的是使用 SearchOption
会导致把无权限的文件也取出来,因此最好是配合 EnumerationOptions
使用。
# searchPattern 不支持多个匹配
由于图片格式是多样的,第一次碰到需要过滤多个后缀文件的情况,正常情况下 *.jpg
这种就行了。
以前一直以为可以用: *.jpg||*.png
—— 实际上并不支持这种功能,要真传入这种格式直接就抛异常了,网上竟然还有说可以用这种方式的文章,不清楚怎么回事。
需要自己实现,例如:
foreach (var pattern in Filter) | |
{ | |
foreach (var item in Directory.EnumerateFiles(dirPath, pattern, SearchOption.AllDirectories)) | |
{ | |
return item; | |
} | |
} |
# WPF .NetCore 如何调取打开目录界面?
见 .net core 3.1 WPF 使用 FolderBrowserDialog 对象打开文件资源管理器选择文件夹
项目增加 <UseWindowsForms>true</UseWindowsForms>
引用后,按照正常代码使用 FolderBrowserDialog
即可:
System.Windows.Forms.FolderBrowserDialog folderDialog = new System.Windows.Forms.FolderBrowserDialog(); | |
if (folderDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) | |
{ | |
TexturesOriginPathText.Text = folderDialog.SelectedPath; | |
} |
打开文件则直接调用 OpenFileDialog
:
Microsoft.Win32.OpenFileDialog fileDialog = new Microsoft.Win32.OpenFileDialog(); | |
if ((bool)fileDialog.ShowDialog()) | |
{ | |
TexturesOriginPathText.Text = fileDialog.FileName; | |
} |
# 降低 ToolTip 显示时间
在目标前添加 ToolTipService.InitialShowDelay="0"
且 ToolTip 可以支持多行:
<Label.ToolTip> | |
<StackPanel Orientation="Vertical"> | |
<TextBlock Text="1"/> | |
<TextBlock Text="2"/> | |
<TextBlock Text="3"/> | |
</StackPanel> | |
</Label.ToolTip> |
# 普通组件也是可以添加事件的
开始想将 Button 表现上设置成 Text 的样子,结果很麻烦,各种自定义样式。后来发现 Text 实际上就可以直接添加点击事件...
# 窗口右上角按钮
例如去掉最大化功能,在 Title 添加: ResizeMode="CanMinimize"
# 限制只能输入数字
TextBox 使用 PreviewTextInput="OnPreviewTextInputLimitNumber
事件:
private void OnPreviewTextInputLimitNumber(object sender, TextCompositionEventArgs e) | |
{ | |
e.Handled = !int.TryParse(e.Text, out int x); | |
} |
不过这种方式只能限制英文键盘,对中文键盘就无效了... 只能说聊胜于无,这种简单的功能 WPF 都没提供吗?总感觉以前写界面的时候好像也用过,难道记错了。
最后在做输入改变时,在对应事件给代码的长宽赋值时,发现其实直接在 TextChanged
事件处理一次也行,例如不合法直接清空:
/// <summary> | |
/// 检测 TextBox 输入是否是纯数字 | |
/// </summary> | |
/// <param name="text"></param> | |
private int CheckTextNumberInput(TextBox text) | |
{ | |
int height; | |
if (int.TryParse(ScaleHeight.Text, out height)) | |
{ | |
return height; | |
} | |
else | |
{ | |
if (!string.IsNullOrEmpty(text.Text)) | |
MessageBox.Show("请输入数字!", "输入不合法", MessageBoxButton.OK, MessageBoxImage.Error); | |
ScaleHeight.Clear(); | |
} | |
return height; | |
} |
注:期间还发现有个 InputScope="Number"
的属性,然而其实完全没什么用,不是限制输入内容的。
# 检测目录权限
.NetCore 可以通过如下方式调用 GetAccessControl
:
System.Security.AccessControl.DirectorySecurity ds = new DirectoryInfo(folderDialog.SelectedPath).GetAccessControl(); |
主要是因为 Directory.EnumerateFiles
枚举文件时,会把无权限的隐藏文件也给枚举出来,结果这个安全检测也不好用,改成 EnumerationOptions
去取文件了。
# BitmapImage 不支持重复利用
只能初始化一次,调用已添加过图片的 BeginInit
会直接抛出异常,也就是每次显示图片都必须重新创建一个对象。
同时 BitmapImage.StreamSource
对象被释放也会导致 Image
显示不出来。
# UI 特性问题多
问题真的很多,简单使用还行,要实现复杂的 UI 效果,查半天都没个能用的...
比如数据绑定,我想把一个 TextBox 组件与类中的一个变量绑定起来,使得修改 TextBox 值对应相当于修改变量 —— 我 Unity 直接写,这都是些啥跟啥,查出来的使用方式也都是要么过时、要么根本不能用的 (虽然也可能跟我版本是 WPF .NetCore 有关)。
感觉可能需要把流程先学习下,直接上手撸,做简单功能还行,想做点高级的特性就力不从心了。
下次要么就把这东西学习流程走一遍再说,要么用 WinForm,做点小东西 WPF 感觉比不上 WinForm 效率好。记得当年用 WinForm 写 Demo 速度刷刷的。
# 与代码双向数据绑定
昨晚回家看了下系统教程,数据与自定义类中的字段做双向 数据绑定
其实很简单,而且还直接支持 TextBox
与 数值
绑定,绑定后虽然不会做输入检测,不过不合法会直接在输入框外显示 红框
- 首先为
DataContext
赋值
DataContext = _helper.ConvertData; |
- 在
ConvertData
实现System.ComponentModel.INotifyPropertyChanged
接口:
public int TestWidth | |
{ | |
get | |
{ | |
return Width; | |
} | |
set | |
{ | |
Width = value; | |
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("TestWidth")); | |
} | |
} | |
private event PropertyChangedEventHandler PropertyChanged; | |
event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged | |
{ | |
add | |
{ | |
PropertyChanged += value; | |
} | |
remove | |
{ | |
PropertyChanged -= value; | |
} | |
} |
- Xaml 处直接绑定
<TextBox Name="ScaleWidth" Text="{Binding TestWidth}" Width="80" VerticalContentAlignment="Center" PreviewTextInput="OnPreviewTextInputLimitNumber" InputScope="Number" TextChanged="ScaleWidth_TextChanged"/> |
感觉还是挺好用的,了解了方法后,像之前通过主动的事件绑定实现的方式,就可以直接改成数据绑定 + 通知的模式了,而且感觉确实方便不少。
于是简单重构了一波代码,分离成 数据+Controller+界面
这种形式。
# 格式化绑定
TextBox Text
使用 StringFormat
Label Content
使用 ContentStringFormat
多数据源绑定:
<MultiBinding StringFormat="{}啦啦啦{0}x啦啦啦{1}啦啦啦}"> | |
<Binding Path="Width"/> | |
<Binding Path="Height"/> | |
</MultiBinding> |
但是默认提供的只支持 TextBox,其它的类型得实现 IMultiValueConverter
来做。
步骤如下:
- 定义一个实现了
IMultiValueConverter
接口的类:
/// <summary> | |
/// 支持非 Text 的多数据源绑定 | |
/// </summary> | |
public class MultiContentConverter : IMultiValueConverter | |
{ | |
object IMultiValueConverter.Convert(object[] values, Type targetType, object parameter, CultureInfo culture) | |
{ | |
try | |
{ | |
return string.Format(parameter.ToString(), values); | |
} | |
catch (Exception ex) | |
{ | |
return ex.Message; | |
} | |
} | |
object[] IMultiValueConverter.ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) | |
{ | |
throw new NotImplementedException(); | |
} | |
} |
- 在
MainWindow.xaml
顶部定义Window.Resources
:
<Window.Resources> | |
<local:MultiContentConverter x:Key="MultiContentConverter" /> | |
</Window.Resources> |
- 通过
MultiBinding
的Converter(转换器)
及ConverterParameter(参数)
指定:
<Label> | |
<MultiBinding Converter="{StaticResource MultiContentConverter}" ConverterParameter="啦啦啦{0}x啦啦啦{1}啦啦啦"> | |
<Binding Path="Width"/> | |
<Binding Path="Height"/> | |
</MultiBinding> | |
</Label> |
# 与组件数据绑定
与父节点绑定:
Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DockPanel}, Path=ActualWidth}" |
与指定 Name 的节点绑定:
Width="{Binding ElementName=ScaleWidth,Path=ActualWidth} |
# 数据绑定中进行计算
这个需要用到一个插件: CalcBinding
直接 NuGet
搜索就有了,有了这个就可以直接在绑定中进行计算,方便了很多。
下载后,在 MainWindow.xaml
最前面加上 xmlns:c="clr-namespace:CalcBinding;assembly=CalcBinding"
,然后就可以使用 c:Binding
进行计算了。
例如上面组件绑定方法,如果想取自身宽度为父容器的一半,就可以这样写:
<GroupBox Header="原图" Width="{c:Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DockPanel}, Path=ActualWidth/2-10}" BorderBrush="Black" Margin="5,5,5,5" BorderThickness="1,1,1,1" > |
另外它还支持直接 Bool
变量转 Visibility
,例如我这有个需求是:CheckBox 勾选后隐藏另外一个组件,否则显示,就可以这样实现:
<CheckBox Name="StayInputFormatToggle" IsChecked="True" Content="保留原格式" Height="26" ToolTipService.InitialShowDelay="0" ToolTip="在处理完毕后,保存的图片与输入格式保持一致。例如修改前是 *.png,修改后也是 *.png。" VerticalAlignment="Center" VerticalContentAlignment="Center"/> | |
<ComboBox Name="OutputFormatComboBox" Visibility="{c:Binding ElementName=StayInputFormatToggle,Path=!IsChecked,FalseToVisibility=Collapsed}" Width="70" Margin="5,0,0,0" VerticalContentAlignment="Center"/> |
更多操作可以看这篇文章,写得挺不错的:在绑定表达式添加计算
# 激活最小化后的窗口
我增加了一个新窗口用于显示『更大』的图片对比效果,这个新界面只有一个作用:左边显示原图、右边显示修改后的图。
然后有个需求就是:没有打开的情况下,直接创建新窗口打开,不过已经打开的情况下,点击按钮应该是将这个界面激活至最上层。
然后就发现正常情况下 Window.Activate()
可以正常激活,但要是窗口已经被最小化了,那么就不行。
查了下是 WindowState 参数问题,最简单是激活前先设置为 WindowState= WindowState.Normal
即可。
不过这样的话,如果之前是最大化状态,重新激活后就会还原。
要是想记录原先的状态,则可以通过注册 Window.StateChanged
简单这样做:
_imageCompareView.StateChanged += (o, x) => _imageCompareViewState = (o as Window).WindowState == WindowState.Minimized ? _imageCompareViewState : (o as Window).WindowState; |
# DockPanel 自动拉伸对齐方式
DockPanel 默认情况下的作用是:自动排版,最后一个元素会拉伸填满剩余空间
剩余空间
的定义其实不止是顺序排版的情况,如果对元素设置过 DockPanel.Dock
,那么就以设置过的元素停靠为准,最后一个元素填满剩余空间。
如下所示:
<DockPanel> | |
<StackPanel DockPanel.Dock="Top"></StackPanel> | |
<StackPanel DockPanel.Dock="Bottom"></StackPanel> | |
<StackPanel></StackPanel> | |
</DockPanel> |
如上所示,第三个 StackPanel
不设置 DockPanel.Dock
,并放在最后,那么就会直接填满中间区域。
(注:必须放在设置过 DockPanel.Dock
的最后)
上面也是我采用的方法,如图所示:
最后的 关于
说明靠下,设置靠上,中间对比元素则填满中间空隙。
# 集合数据绑定
列表类型的组件元素,不能直接绑定通用的 List
容器 —— 我就尝试了下,就发现当 List
发生变动时,绑定 UI 根本不会同时发生改变。
经过研究发现像 ListBox、ListView、DataGrid
这种容器类组件元素,绑定的数据源必须是 可监听
的,例如 ObservableCollection
才能正确接收集合改变时的通知。
# 子窗口初始位置
可以通过 WindowStartupLocation
进行控制
BatchProgressView view = new BatchProgressView(); | |
view.Owner = this; | |
view.WindowStartupLocation = WindowStartupLocation.CenterOwner; |
需要注意的是 CenterOwner
选项的情况下,必须设置 Owner
才有效,否则也是满屏乱飞。