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

使用 WPF 的 EXIF 比较工具

starIconstarIconstarIconstarIconstarIcon

5.00/5 (14投票s)

2010年4月12日

CPOL

8分钟阅读

viewsIcon

73693

downloadIcon

1108

Exif Compare Utility 是一个相当于WinDiff的图像文件比较工具,它比较Exif元数据并显示差异和相似之处。该应用程序使用WPF和MVVM编写。

图1:应用程序运行状态 - 所有部分均已展开(截图未显示)

图2:截图显示已折叠的部分以及通过右键菜单复制文本的功能

引言

Exif Compare Utility 是我为满足特定需求而编写的应用程序。它是一个相当于WinDiff的图像文件比较工具,能够比较Exif元数据并显示差异和相似之处。该应用程序使用WPF和MVVM编写,并利用了我编写的ExifReader库。在本文中,我将简要介绍如何使用该应用程序,并讨论一些有趣的编码实现细节。

代码格式化

本文中的代码片段为了适应Code Project 600像素的宽度要求,已被强制换行。实际源代码文件中的代码格式化要更加赏心悦目。

使用该实用程序

从菜单或工具栏中选择Compare Files,您将看到两个文件打开对话框,用于选择两个图像文件。选择文件后,您将看到Exif比较结果(参见图1),结果分为四个可折叠的部分。

  1. 不同的Exif属性
  2. 仅存在于左侧图像中的Exif属性
  3. 仅存在于右侧图像中的Exif属性
  4. 相同的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; }
}

其中的几个属性(IsLeftEmptyIsRightEmpty)是为了方便数据绑定而提供的。有些人可能会想,这是否违反了MVVM模式,因为我们在Model类中拥有纯粹为了View方便的属性。但请记住,View对这个Model类一无所知。数据关联是在运行时由WPF数据绑定框架完成的。如果您对这些问题非常严谨,可以将这个类移到View-Model中,但我个人更倾向于选择最简单的方法,而不是过于教条和复杂的方法。

属性比较基于Exif标签和Exif属性值的比较。由于ExifReaderExifProperty类都不支持相等性检查,因此我实现了以下比较器类。

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,它们通过LeftImageUserControlViewModelRightImageUserControlViewModel属性返回。请注意,用于显示“关于”对话框的代码,其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的当前宽度。这是顶部的高亮显示代码。

数据触发器显示了IsLeftEmptyIsRightEmpty属性如何根据属性是否存在来改变不透明度。这正是我之前在讨论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日 - 文章首次发布
© . All rights reserved.