使用 WPF 的 EXIF 比较工具





5.00/5 (14投票s)
Exif Compare Utility 是一个相当于WinDiff的图像文件比较工具,它比较Exif元数据并显示差异和相似之处。该应用程序使用WPF和MVVM编写。
引言
Exif Compare Utility 是我为满足特定需求而编写的应用程序。它是一个相当于WinDiff的图像文件比较工具,能够比较Exif元数据并显示差异和相似之处。该应用程序使用WPF和MVVM编写,并利用了我编写的ExifReader库。在本文中,我将简要介绍如何使用该应用程序,并讨论一些有趣的编码实现细节。
代码格式化
本文中的代码片段为了适应Code Project 600像素的宽度要求,已被强制换行。实际源代码文件中的代码格式化要更加赏心悦目。
使用该实用程序
从菜单或工具栏中选择Compare Files,您将看到两个文件打开对话框,用于选择两个图像文件。选择文件后,您将看到Exif比较结果(参见图1),结果分为四个可折叠的部分。
- 不同的Exif属性
- 仅存在于左侧图像中的Exif属性
- 仅存在于右侧图像中的Exif属性
- 相同的Exif属性
使用Clear Files(菜单/工具栏)来清除选择。您可以右键单击某个条目以调出右键菜单(参见图2),该菜单只有一个条目——Copy命令,用于将文本表示复制到剪贴板。
有趣的编码细节
WPF应用程序是使用VS 2010 RC编写的,并采用了MVVM模式。它使用我的ExifReader库来读取Exif数据,但在模型类中实现了比较功能。我将引用ExifReader库中的几个类,为了避免重复,我将不在本文中描述它们。想查阅的读者可以阅读ExifReader文章。请注意,本文的源代码下载中也包含了ExifReader项目,因此您无需任何额外的下载即可编译和运行该应用程序。
模型
有一个名为ExifPropertyComparison
的类,它表示一个特定的Exif属性。根据属性是仅存在于一个图像还是两个图像中,它将包含左侧或右侧的值,或两者都有。
internal class ExifPropertyComparison
{
private const string EMPTYSTRING = "N/A";
public ExifPropertyComparison(ExifProperty exifProperty)
: this(exifProperty, exifProperty)
{
}
public ExifPropertyComparison(ExifProperty left, ExifProperty right)
{
if (left == null && right == null)
{
throw new InvalidOperationException(
"Both arguments cannot be null.");
}
if (left == null)
{
this.IsLeftEmpty = true;
this.LeftValue = EMPTYSTRING;
}
else
{
this.LeftValue = left.ToString();
}
if (right == null)
{
this.IsRightEmpty = true;
this.RightValue = EMPTYSTRING;
}
else
{
this.RightValue = right.ToString();
}
this.TagName = (left ?? right).ExifTag.ToString();
}
public bool IsLeftEmpty { get; private set; }
public bool IsRightEmpty { get; private set; }
public string LeftValue { get; private set; }
public string RightValue { get; private set; }
public string TagName { get; private set; }
}
其中的几个属性(IsLeftEmpty
和IsRightEmpty
)是为了方便数据绑定而提供的。有些人可能会想,这是否违反了MVVM模式,因为我们在Model类中拥有纯粹为了View方便的属性。但请记住,View对这个Model类一无所知。数据关联是在运行时由WPF数据绑定框架完成的。如果您对这些问题非常严谨,可以将这个类移到View-Model中,但我个人更倾向于选择最简单的方法,而不是过于教条和复杂的方法。
属性比较基于Exif标签和Exif属性值的比较。由于ExifReader
和ExifProperty
类都不支持相等性检查,因此我实现了以下比较器类。
internal class ExifPropertyTagEqualityComparer
: IEqualityComparer<ExifProperty>
{
public bool Equals(ExifProperty x, ExifProperty y)
{
return x.ExifTag == y.ExifTag;
}
public int GetHashCode(ExifProperty obj)
{
return (int)obj.ExifTag;
}
}
这个比较器相当简单,因为它只比较ExifTag
属性。
internal class ExifPropertyValueEqualityComparer
: IEqualityComparer<ExifProperty>
{
public bool Equals(ExifProperty x, ExifProperty y)
{
if (x.ExifValue.Count != y.ExifValue.Count
|| x.ExifDatatype != y.ExifDatatype)
return false;
bool equal = true;
object[] xValues = x.ExifValue.Values.Cast<object>().ToArray();
object[] yValues = y.ExifValue.Values.Cast<object>().ToArray();
for (int i = 0; i < xValues.Length; i++)
{
if (!(equal = xValues[i].Equals(yValues[i])))
{
break;
}
}
return equal;
}
public int GetHashCode(ExifProperty obj)
{
return (int)obj.ExifTag * (int)obj.ExifDatatype
+ obj.ExifValue.Count;
}
}
由于Exif值通常可能包含多个值,因此比较Exif值需要更多的代码。一旦有了比较器,就可以相对容易地编写实际的Exif比较代码,并利用一些LINQ用法。这在ExifCompareModel
类中实现。
internal class ExifCompareModel
{
private static ExifPropertyTagEqualityComparer tagEqualityComparer
= new ExifPropertyTagEqualityComparer();
private static ExifPropertyValueEqualityComparer valueEqualityComparer
= new ExifPropertyValueEqualityComparer();
private List<ExifPropertyComparison> onlyInLeftProperties
= new List<ExifPropertyComparison>();
public List<ExifPropertyComparison> OnlyInLeftProperties
{
get { return onlyInLeftProperties; }
}
private List<ExifPropertyComparison> onlyInRightProperties
= new List<ExifPropertyComparison>();
public List<ExifPropertyComparison> OnlyInRightProperties
{
get { return onlyInRightProperties; }
}
private List<ExifPropertyComparison> identicalProperties
= new List<ExifPropertyComparison>();
public List<ExifPropertyComparison> IdenticalProperties
{
get { return identicalProperties; }
}
private List<ExifPropertyComparison> differingProperties
= new List<ExifPropertyComparison>();
public List<ExifPropertyComparison> DifferingProperties
{
get { return differingProperties; }
}
/// <summary>
/// Initializes a new instance of the ExifCompareModel class.
/// </summary>
public ExifCompareModel(string leftFileName, string rightFileName)
{
var leftproperties = new ExifReader(
leftFileName).GetExifProperties();
var rightproperties = new ExifReader(
rightFileName).GetExifProperties();
var onlyInLeft = leftproperties.Except(
rightproperties, tagEqualityComparer);
this.onlyInLeftProperties = onlyInLeft.Select(
p => new ExifPropertyComparison(p, null)).ToList();
var onlyInRight = rightproperties.Except(
leftproperties, tagEqualityComparer);
this.onlyInRightProperties = onlyInRight.Select(
p => new ExifPropertyComparison(null, p)).ToList();
var commonpropertiesInLeft = leftproperties.Except(
onlyInLeft, tagEqualityComparer).OrderBy(
exprop => exprop.ExifTag).ToArray();
var commonpropertiesInRight = rightproperties.Except(
onlyInRight, tagEqualityComparer).OrderBy(
exprop => exprop.ExifTag).ToArray();
for (int i = 0; i < commonpropertiesInLeft.Length; i++)
{
if (valueEqualityComparer.Equals(
commonpropertiesInLeft[i], commonpropertiesInRight[i]))
{
this.identicalProperties.Add(
new ExifPropertyComparison(commonpropertiesInLeft[i]));
}
else
{
this.differingProperties.Add(
new ExifPropertyComparison(
commonpropertiesInLeft[i],
commonpropertiesInRight[i]));
}
}
}
}
该类公开了四个List<ExifPropertyComparison>
类型的属性,分别代表四种属性类别——仅存在于左侧或右侧图像中的属性、同时存在于两个图像中且值相同的属性、以及同时存在于两个图像中但值不同的属性。
ViewModel
这是主窗口View-Model的代码(部分截取)。
internal class MainWindowViewModel : ViewModelBase
{
. . .
private ImageUserControlViewModel leftImageUserControlViewModel
= ImageUserControlViewModel.Empty;
private ImageUserControlViewModel rightImageUserControlViewModel
= ImageUserControlViewModel.Empty;
/// <summary>
/// Initializes a new instance of the MainWindowViewModel class.
/// </summary>
public MainWindowViewModel()
{
this.OnlyInLeftProperties =
new ObservableCollection<ExifPropertyComparison>();
this.OnlyInRightProperties =
new ObservableCollection<ExifPropertyComparison>();
this.IdenticalProperties =
new ObservableCollection<ExifPropertyComparison>();
this.DifferingProperties =
new ObservableCollection<ExifPropertyComparison>();
}
public ObservableCollection<ExifPropertyComparison>
OnlyInLeftProperties { get; private set; }
public ObservableCollection<ExifPropertyComparison>
OnlyInRightProperties { get; private set; }
public ObservableCollection<ExifPropertyComparison>
IdenticalProperties { get; private set; }
public ObservableCollection<ExifPropertyComparison>
DifferingProperties { get; private set; }
public ICommand ExitCommand
{
get
{
return exitCommand ??
(exitCommand = new DelegateCommand(
() => Application.Current.Shutdown()));
}
}
public ICommand CompareFilesCommand
{
get
{
return compareFilesCommand ??
(compareFilesCommand = new DelegateCommand(BrowseForFiles));
}
}
public ICommand ClearFilesCommand
{
get
{
return clearFilesCommand ??
(clearFilesCommand =
new DelegateCommand(ClearFiles, CanClearFiles));
}
}
public ICommand AboutCommand
{
get
{
return aboutCommand ??
(aboutCommand =
new DelegateCommand<Window>(
(owner) => new AboutWindow()
{ Owner = owner }.ShowDialog()));
}
}
public ICommand ExifCompareCopy
{
get
{
return exifCompareCopy ??
(exifCompareCopy =
new DelegateCommand<ExifPropertyComparison>(
ExifCompareCopyToClipBoard));
}
}
public ImageUserControlViewModel LeftImageUserControlViewModel
{
get
{
return leftImageUserControlViewModel;
}
set
{
if (this.leftImageUserControlViewModel.FilePath
!= value.FilePath)
{
this.leftImageUserControlViewModel = value;
this.FirePropertyChanged("LeftImageUserControlViewModel");
}
}
}
public ImageUserControlViewModel RightImageUserControlViewModel
{
get
{
return rightImageUserControlViewModel;
}
set
{
if (this.rightImageUserControlViewModel.FilePath
!= value.FilePath)
{
this.rightImageUserControlViewModel = value;
this.FirePropertyChanged("RightImageUserControlViewModel");
}
}
}
public void BrowseForFiles()
{
OpenFileDialog fileDialog = new OpenFileDialog()
{
Filter = "Image Files(*.PNG;*.JPG)|*.PNG;*.JPG;"
};
if (fileDialog.ShowDialog().GetValueOrDefault())
{
string tempLeftFilePath = fileDialog.FileName;
if (fileDialog.ShowDialog().GetValueOrDefault())
{
try
{
ExifCompareModel exifCompare = new ExifCompareModel(
tempLeftFilePath, fileDialog.FileName);
Repopulate(this.OnlyInLeftProperties,
exifCompare.OnlyInLeftProperties);
Repopulate(this.OnlyInRightProperties,
exifCompare.OnlyInRightProperties);
Repopulate(this.IdenticalProperties,
exifCompare.IdenticalProperties);
Repopulate(this.DifferingProperties,
exifCompare.DifferingProperties);
this.LeftImageUserControlViewModel =
new ImageUserControlViewModel(tempLeftFilePath);
this.RightImageUserControlViewModel =
new ImageUserControlViewModel(fileDialog.FileName);
}
catch (ExifReaderException ex)
{
MessageBox.Show(ex.Message, "Error reading EXIF data",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
}
public void ExifCompareCopyToClipBoard(
ExifPropertyComparison parameter)
{
var stringData = String.Format(
"TagName = {0}, LeftValue = {1}, RightValue = {2}",
parameter.TagName, parameter.LeftValue, parameter.RightValue);
Clipboard.SetText(stringData);
}
private void Repopulate(
ObservableCollection<ExifPropertyComparison> observableCollection,
List<ExifPropertyComparison> list)
{
observableCollection.Clear();
list.ForEach(p => observableCollection.Add(p));
}
public void ClearFiles()
{
this.OnlyInLeftProperties.Clear();
this.OnlyInRightProperties.Clear();
this.IdenticalProperties.Clear();
this.DifferingProperties.Clear();
this.LeftImageUserControlViewModel = ImageUserControlViewModel.Empty;
this.RightImageUserControlViewModel = ImageUserControlViewModel.Empty;
}
public bool CanClearFiles()
{
return this.LeftImageUserControlViewModel
!= ImageUserControlViewModel.Empty;
}
}
View-Model公开了四个ObservableCollection<ExifPropertyComparison>
属性,这些属性从Model返回的相应属性中填充。预览图像视图有自己的View-Model,它们通过LeftImageUserControlViewModel
和RightImageUserControlViewModel
属性返回。请注意,用于显示“关于”对话框的代码,其Owner
窗口是通过命令参数传递的。稍后在显示相应的View代码时,我将对此进行讨论。
internal class ImageUserControlViewModel : ViewModelBase
{
. . .
private static ImageUserControlViewModel empty =
new ImageUserControlViewModel();
public static ImageUserControlViewModel Empty
{
get { return ImageUserControlViewModel.empty; }
}
private ImageUserControlViewModel()
{
}
/// <summary>
/// Initializes a new instance of the ImageUserControlViewModel class.
/// </summary>
public ImageUserControlViewModel(string filePath)
{
this.fileName = Path.GetFileName(filePath);
this.filePath = filePath;
}
public string FileName
{
get
{
return fileName ?? "No image selected";
}
}
public string FilePath
{
get
{
return filePath;
}
}
public override string ToString()
{
return FilePath ?? "No image selected";
}
}
理论上,我可以将所有这些都放在主View-Model中,但我这样做是为了方便和更简单的代码组织。此外,还有一个关于对话框的View-Model类,其中信息显示基于程序集的版本信息。我使用了一种相当常见的MVVM技术,即在View-Model中引发一个事件,该事件由View处理以关闭“关于”对话框。像Josh Smith这样的人在他的各种文章中都曾撰写过关于这项技术的博客。这与我用于将Owner窗口传递给显示“关于”对话框的代码的技术不同。我也可以在这里使用相同的技术,但我选择这样做是为了体验这两种技术。后一种方法存在一个缺点,即需要在View类中编写代码,这可能会冒犯MVVM的纯粹主义者,尽管严格来说,这段代码唯一的作用就是关闭对话框。前者技术在View中无需编写代码,但需要一些相当笨拙的绑定代码来将Owner窗口作为命令参数传递,有些人可能不愿意这样做。这是“关于”对话框View-Model的代码。
internal class AboutWindowViewModel : ViewModelBase
{
private FileVersionInfo fileVersionInfo;
private ICommand closeCommand;
/// <summary>
/// Initializes a new instance of the AboutWindowViewModel class.
/// </summary>
public AboutWindowViewModel()
{
fileVersionInfo = FileVersionInfo.GetVersionInfo(
Assembly.GetExecutingAssembly().Location);
}
public event EventHandler CloseRequested;
private void FireCloseRequested()
{
EventHandler handler = CloseRequested;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
public ICommand CloseCommand
{
get
{
return closeCommand ?? (
closeCommand = new DelegateCommand(() => FireCloseRequested()));
}
}
public string Comments
{
get
{
return fileVersionInfo.Comments;
}
}
public string InternalName
{
get
{
return fileVersionInfo.InternalName;
}
}
public string FileDescription
{
get
{
return fileVersionInfo.FileDescription;
}
}
public string FileVersion
{
get
{
return fileVersionInfo.FileVersion;
}
}
public string LegalCopyright
{
get
{
return fileVersionInfo.LegalCopyright;
}
}
}
视图
关于对话框的Xaml中没有值得关注的内容,除了构造函数中处理View-Model事件的代码(我之前提到过的)。
public AboutWindow()
{
InitializeComponent();
var viewModel = new AboutWindowViewModel();
viewModel.CloseRequested += (s, e) => Close();
this.DataContext = viewModel;
}
这是ImageUserControl
视图类的,经过人工强制换行的Xaml代码。
<UserControl x:Class="ExifCompare.ImageUserControl"
. . .
d:DesignHeight="264" d:DesignWidth="320" Background="Transparent">
<dropShadow:SystemDropShadowChrome CornerRadius="15, 15, 0, 0" Width="320">
<Grid Background="Transparent">
<StackPanel Orientation="Vertical" Tag="{Binding}">
<StackPanel.ToolTip>
<ToolTip
DataContext="{Binding Path=PlacementTarget,
RelativeSource={x:Static RelativeSource.Self}}"
Content="{Binding Tag}" FontSize="14"
Background="#FF2B4E2B" Foreground="YellowGreen" />
</StackPanel.ToolTip>
<Border BorderThickness="0" Background="#FF2B4E2B"
Width="320" CornerRadius="15, 15, 0, 0">
<TextBlock x:Name="textBlock" Padding="5,2,4,1"
Background="Transparent" Text="{Binding FileName}"
Width="320" Height="24" FontSize="15" Foreground="White" />
</Border>
<Border Background="#FF427042">
<Image VerticalAlignment="Top" Width="320" Height="240"
Source="{Binding FilePath}" />
</Border>
</StackPanel>
</Grid>
</dropShadow:SystemDropShadowChrome>
</UserControl>
查看高亮显示的代码,那里设置了工具提示。最初,我没有类似的代码,而是简单地绑定到当前对象,因此只会看到ImageUserControlViewModel
对象的ToString
结果。我发现这只在代码首次调用时有效,当我打开新文件时,工具提示不会更新。经过一番努力和Google搜索后,我才意识到ToolTip不会继承数据上下文,因为它不属于视觉树。在这种情况下,PlacementTarget
将是包含它的StackPanel
,并且我将其Tag
属性绑定到ImageUserControlViewModel
对象,这也是工具提示弹出和调用数据绑定时获得的对象。问题解决了。
这是主窗口的Xaml。一些绑定字符串的换行可能会导致代码错误。但我不期望任何人复制代码并粘贴到IDE中——因为您始终可以查看提供的源代码下载。
<nsmvvm:SystemMenuWindow x:Class="ExifCompare.MainWindow"
. . .
Title="Exif Compare Utility" Height="750" Width="1000"
MinHeight="300" MinWidth="750">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ExifCompareResourceDictionary.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<nsmvvm:SystemMenuWindow.MenuItems>
<nsmvvm:SystemMenuItem Command="{Binding AboutCommand}"
CommandParameter="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor, AncestorType=Window}}"
Header="About" Id="100" />
</nsmvvm:SystemMenuWindow.MenuItems>
我使用了我的SystemMenuWindow类来将“关于”菜单项添加到系统窗口。但那里有趣的代码是CommandParameter
绑定。我找到了第一个Window祖先并将其传递给命令,因为“关于”对话框需要设置一个所有者窗口来进行居中。这样我们就可以避免View-Model对View的反向引用,尽管在某些MVVM实现中,View-Model确实持有View的引用。我也可以在View-Model中实现一个由View处理以传递owner的事件,但我认为这样做更简单。
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="60" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Menu Height="25" VerticalAlignment="Top">
<MenuItem Header="File">
<MenuItem Command="{Binding CompareFilesCommand}"
Header="Compare Files" />
<MenuItem Command="{Binding ClearFilesCommand}"
Header="Clear Files" />
<MenuItem Command="{Binding ExitCommand}"
Header="Exit" />
</MenuItem>
<MenuItem Header="Help">
<MenuItem Command="{Binding AboutCommand}"
CommandParameter="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType=Window}}"
Header="About" />
</MenuItem>
</Menu>
<ToolBar Height="35" VerticalAlignment="Bottom" FontSize="14">
<Button Command="{Binding CompareFilesCommand}">
<StackPanel Orientation="Horizontal">
<Image Width="32"
Source="/ExifCompare;component/Images/CompareFiles.png"></Image>
<TextBlock VerticalAlignment="Center">
Compare Files</TextBlock>
</StackPanel>
</Button>
<Button Command="{Binding ClearFilesCommand}">
<StackPanel Orientation="Horizontal">
<Image Width="32"
Source="/ExifCompare;component/Images/Clear.png">
</Image>
<TextBlock VerticalAlignment="Center">
Clear Files</TextBlock>
</StackPanel>
</Button>
<Button Command="{Binding AboutCommand}"
CommandParameter="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor, AncestorType=Window}}">
<StackPanel Orientation="Horizontal">
<Image Width="32"
Source="/ExifCompare;component/Images/HelpAbout.png">
</Image>
<TextBlock VerticalAlignment="Center">About</TextBlock>
</StackPanel>
</Button>
<ToolBar.Background>
<LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">
<GradientStop Color="#FF9DB79D" Offset="0" />
<GradientStop Color="#FF19380B" Offset="1" />
</LinearGradientBrush>
</ToolBar.Background>
</ToolBar>
</Grid>
菜单和工具栏实现了相同的功能,并使用了相同的View-Model命令。
<ScrollViewer Grid.Row="1">
<ScrollViewer.Background>
<LinearGradientBrush EndPoint="0,0" StartPoint="1,0">
<GradientStop Color="#FFA7B7A7" Offset="0" />
<GradientStop Color="#FF195219" Offset="1" />
</LinearGradientBrush>
</ScrollViewer.Background>
<Grid Margin="20">
<StackPanel Orientation="Vertical">
<Expander VerticalAlignment="Top" Header="Preview Images"
IsExpanded="True" ExpandDirection="Down"
FontSize="16" FontWeight="Bold">
<Grid VerticalAlignment="Stretch"
Background="#FFB7D4B7">
<StackPanel Orientation="Horizontal"
Height="260" VerticalAlignment="Top"
HorizontalAlignment="Center">
<local:ImageUserControl
DataContext="{Binding LeftImageUserControlViewModel}"
Margin="5" />
<local:ImageUserControl
DataContext="{Binding RightImageUserControlViewModel}"
Margin="5" />
</StackPanel>
</Grid>
</Expander>
图像预览用户控件在Xaml中指定了DataContext
,并绑定到相应的View-Model对象。
<Expander VerticalAlignment="Top"
Header="Differing EXIF properties"
IsExpanded="True" ExpandDirection="Down"
FontSize="16" FontWeight="Bold">
<Grid VerticalAlignment="Stretch" MinHeight="40"
Background="#FFB7D4B7" TextBlock.FontSize="13"
TextBlock.FontWeight="Normal">
<ListBox ItemsSource="{Binding DifferingProperties}"
Style="{StaticResource ExifListBox}"
ItemTemplate="{StaticResource ExifListBoxItemTemplate}"
ItemContainerStyle="{StaticResource ExifListBoxItem}">
</ListBox>
</Grid>
</Expander>
<Expander VerticalAlignment="Top"
Header="EXIF Properties only in left image"
IsExpanded="True" ExpandDirection="Down"
FontSize="16" FontWeight="Bold">
<Grid VerticalAlignment="Stretch" MinHeight="40"
Background="#FFB7D4B7" TextBlock.FontSize="13"
TextBlock.FontWeight="Normal">
<ListBox ItemsSource="{Binding OnlyInLeftProperties}"
Style="{StaticResource ExifListBox}"
ItemTemplate="{StaticResource ExifListBoxItemTemplate}"
ItemContainerStyle="{StaticResource ExifListBoxItem}">
</ListBox>
</Grid>
</Expander>
<Expander VerticalAlignment="Top"
Header="EXIF Properties only in right image"
IsExpanded="True" ExpandDirection="Down"
FontSize="16" FontWeight="Bold">
<Grid VerticalAlignment="Stretch" MinHeight="40"
Background="#FFB7D4B7"
TextBlock.FontSize="13"
TextBlock.FontWeight="Normal">
<ListBox ItemsSource="{Binding OnlyInRightProperties}"
Style="{StaticResource ExifListBox}"
ItemTemplate="{StaticResource ExifListBoxItemTemplate}"
ItemContainerStyle="{StaticResource ExifListBoxItem}">
</ListBox>
</Grid>
</Expander>
<Expander VerticalAlignment="Top"
Header="Identical EXIF properties"
IsExpanded="True" ExpandDirection="Down"
FontSize="16" FontWeight="Bold">
<Grid VerticalAlignment="Stretch" MinHeight="40"
Background="#FFB7D4B7"
TextBlock.FontSize="13"
TextBlock.FontWeight="Normal">
<ListBox ItemsSource="{Binding IdenticalProperties}"
Style="{StaticResource ExifListBox}"
ItemTemplate="{StaticResource ExifListBoxItemTemplate}"
ItemContainerStyle="{StaticResource ExifListBoxItem}">
</ListBox>
</Grid>
</Expander>
. . .
属性比较是通过样式化和模板化的列表框显示的。它们都包装在Expander
块中,并且Expander
也经过了不同的样式设置,因为默认的外观与其余的样式/主题不协调。我将简要介绍一些在单独的资源字典中定义的样式和模板。
样式和模板
为了自定义Expander控件,我开始参考了MSDN示例。我移除了一些部分(例如,那些处理禁用状态的部分),然后根据我的偏好进行了自定义。
<ControlTemplate x:Key="ExpanderToggleButton"
TargetType="ToggleButton">
<Border Name="Border"
CornerRadius="2,0,0,0"
Background="{StaticResource BasicBrush}"
BorderBrush="{StaticResource NormalBorderBrush}"
BorderThickness="0,0,1,0">
<Path Name="Arrow"
Fill="{StaticResource GlyphBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M 0 0 L 8 8 L 16 0 Z"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="ToggleButton.IsMouseOver" Value="true">
<Setter TargetName="Border" Property="Background"
Value="{StaticResource DarkBrush}" />
</Trigger>
<Trigger Property="IsPressed" Value="true">
<Setter TargetName="Border" Property="Background"
Value="{StaticResource PressedBrush}" />
</Trigger>
<Trigger Property="IsChecked" Value="true">
<Setter TargetName="Arrow" Property="Data"
Value="M 0 8 L 8 0 L 16 8 Z" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
这是Expander
控件上ToggleButton
的控件模板。箭头及其翻转版本都使用基本的Path
控件创建。
<Style TargetType="Expander">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Expander">
<Grid>
. . .
<Border Name="Border"
Grid.Row="0"
Background="{StaticResource LightBrush}"
BorderBrush="{StaticResource NormalBorderBrush}"
BorderThickness="1"
CornerRadius="2,2,0,0" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="25" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ToggleButton
IsChecked="{Binding Path=IsExpanded,Mode=TwoWay,
RelativeSource={RelativeSource TemplatedParent}}"
OverridesDefaultStyle="True"
Template="{StaticResource ExpanderToggleButton}" />
<ContentPresenter Grid.Column="1"
Margin="4"
ContentSource="Header"
RecognizesAccessKey="True" />
</Grid>
</Border>
<Border Name="Content"
Grid.Row="1"
Background="{StaticResource WindowBackgroundBrush}"
BorderBrush="{StaticResource SolidBorderBrush}"
BorderThickness="1,0,1,1"
CornerRadius="0,0,2,2" >
<ContentPresenter />
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Setter TargetName="ContentRow" Property="Height"
Value="{Binding ElementName=Content,Path=DesiredHeight}" />
</Trigger>
</ControlTemplate.Triggers>
. . .
在Expander
的控件模板中,我们使用了自定义的ToggleButton
模板。
<Style TargetType="{x:Type ListBox}" x:Key="ExifListBox">
. . .
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<ItemsPresenter />
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={x:Static
RelativeSource.Self}, Path=Items.Count}" Value="0">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<TextBlock Foreground="#FF183E11"
FontSize="14">This section is empty.</TextBlock>
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
这是我用于ListBox
的样式,以便在ListBox
为空时可以显示自定义的空消息。这是通过绑定实现的,我将绑定到Items
属性的Count
属性。
<DataTemplate x:Key="ExifListBoxItemTemplate">
<Grid Background="#FF6E9A96" HorizontalAlignment="Stretch"
x:Name="mainItemGrid"
TextBlock.FontSize="15"
MaxWidth="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType=ListBox}, Path=ActualWidth}">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding LeftValue}" Grid.Column="0" x:Name="leftValue"
Padding="5" TextTrimming="CharacterEllipsis" />
<TextBlock Text="{Binding TagName}" Grid.Column="1" x:Name="tagName"
Padding="5" TextTrimming="CharacterEllipsis"
Background="#FF3B726C" />
<TextBlock Text="{Binding RightValue}" Grid.Column="2" x:Name="rightValue"
Padding="5" TextTrimming="CharacterEllipsis" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsLeftEmpty}" Value="True">
<Setter TargetName="leftValue" Property="Opacity" Value="0.3" />
</DataTrigger>
<DataTrigger Binding="{Binding IsRightEmpty}" Value="True">
<Setter TargetName="rightValue" Property="Opacity" Value="0.3" />
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource
Mode=FindAncestor, AncestorType=ListBoxItem}, Path=IsSelected}" Value="True">
<Setter TargetName="mainItemGrid"
Property="Background" Value="#FF526C66" />
<Setter TargetName="tagName"
Property="Background" Value="#FF254843" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
这是ListBoxItem
的数据模板——我们实际在这里显示属性比较数据。有些Exif属性的ExifProperty
类中的ToString
实现会返回一个十六进制字节的显示字符串。这会导致水平滚动,为了避免这种情况,我使用绑定确保MaxWidth
等于ListBox
的当前宽度。这是顶部的高亮显示代码。
数据触发器显示了IsLeftEmpty
和IsRightEmpty
属性如何根据属性是否存在来改变不透明度。这正是我之前在讨论Model实现时所指的。
<ContextMenu x:Key="ListBoxItemContextMenu">
<MenuItem Header="Copy"
Command="{Binding RelativeSource={
RelativeSource Mode=FindAncestor,
AncestorType=ListBox}, Path=DataContext.ExifCompareCopy}"
CommandParameter="{Binding}"/>
</ContextMenu>
<Style TargetType="{x:Type ListBoxItem}" x:Key="ExifListBoxItem">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}"
Color="Transparent"/>
<SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}"
Color="Transparent"/>
</Style.Resources>
<Setter Property="ContextMenu" Value="{StaticResource ListBoxItemContextMenu}" />
</Style>
这是另一个我花了几分钟才弄明白的代码片段。ListBoxItem
的数据上下文是与之关联的ExifPropertyComparison
对象。由于我想绑定到View-Model中的一个命令,我必须首先通过使用FindAncestor
来查找与ListBox
相关联的数据上下文(这将是View-Model实例),从而获取正确的数据源。
结论
我想本文中没有太多惊天动地的内容,但我主要是想讨论我遇到的问题以及如何解决它们。显然,如果您认为有更好的解决方法,请随时使用文章论坛提出您的建议、批评和想法。谢谢。
历史
- 2009年4月10日 - 文章首次发布