天天看點

閑話WinFrom與WPF(3) 控件篇——CheckedListBox

我曾經寫過一個篩選的Demo,裡面有一個清單選擇控件:

閑話WinFrom與WPF(3) 控件篇——CheckedListBox

這次隻是說一個全選功能。

我曾經以為應該有更優雅的方式去實作全選功能,即使到現在我還是沒有找到,有時候人們會告訴你不要對一個問題過于糾結,隻要實作了功能就成,但是程式員往往不是這麼想。

當我開始再一次用WinForm實作這個功能時,發現還是隻能使用原來的方法,沒有更好的方案去做這個。這要說還是由于WinForm過于死闆,無法如WPF一般簡單的實作自定義。

鑒于此Demo為本系列文章的第一篇WPF的Demo,這裡盡量說的自己感覺易懂,僅僅展示重寫模闆的一些問題。

原文位址:http://nanqi.info/blog/2013/03/31/winform-wpf-3/

WinForm中的實作方案

代碼幾乎與那個篩選的Demo沒有改變多少,唯一不同的是我發現其實不必自己去控制CheckedBox的選擇(AutoCheck),同時也發現其實自己當時做的也沒有錯,因為如果當CheckedBox的CheckState屬性為indeterminate時,想讓再點一次讓其選中,那麼還必須我這麼做(其實也從側面反應,自己不止實作多餘,而且做錯了)。

我依舊使用CheckedListBox的ItemCheck事件,代碼如下:

private void chbxList_ItemCheck(object sender, ItemCheckEventArgs e)
{
    this.chbxAll.CheckedChanged -= this.chbxAll_CheckedChanged;
    if (e.NewValue == CheckState.Checked && chbxList.CheckedItems.Count + 1 >= chbxList.Items.Count)
    {
        chbxAll.CheckState = CheckState.Checked;
    }
    else if (e.NewValue == CheckState.Unchecked && chbxList.CheckedItems.Count - 1 <= 0)
    {
        chbxAll.CheckState = CheckState.Unchecked;
    }
    else
    {
        chbxAll.CheckState = CheckState.Indeterminate;
    }
    this.chbxAll.CheckedChanged += this.chbxAll_CheckedChanged;
}
           

我一直耿耿于懷的是-=和+=的問題,這次我突然想起,當初為什麼要使用ItemCheck事件,而不是SelectedIndexChanged,這次試了試,本以為又發現了原來代碼中可以優化的地方,最後還是知道當初為什麼不用。

和我當年使用DataGridView時在上面增加一個CheckBox列同樣的問題:輕按兩下會出問題。

有興趣的同學可以試試,我這裡就不再示範了,類似的問題其實還有很多,涉及WinForm與WPF,但是往往都是,WPF中發現問題,可以很容易的使用其他方法解決,但是WinForm中解決起來就不是那麼容易了。

WPF中實作一個CheckedListBox

WPF中是沒有這麼一個控件的,但是幾乎使用過幾次WPF的人,都能實作這麼一個控件,說控件就成了WinForm中的方式,這裡可能僅僅換個樣式就可以實作。

WPF中的xaml,我到現在仍有許多問題沒有弄懂,我深切的知道一個剛接觸WPF的人,看到一大堆xaml那種恐懼的心情,而且這也是本系列文章第一次使用WPF,我想還是有必要說一些基本的問題(但不是基礎)。

首先當你拿到源碼,你會發現我使用ListBox實作,這個選擇很多,這裡不必糾結于此,你又會發現我将這個樣式寫了兩份,而且一個存放在一個ResourceDictionary中,在App.xaml中引入了它。

要說其實這種方式與帶Key值的差不多,但是還有不少差別,如果把這個例子放在一個項目中,那麼沒有帶Key值顯然是錯的,因為不可能讓所有ListBox都成為一個CheckedListBox,但是我是很讨厭在一個控件上看到很多屬性的,至少當它放在界面上的時候,能少則少,至少本例中沒有任何問題。我最後放在界面上會是這樣:

<ListBox Name="checkListBox"
         Canvas.Left="10"
         Canvas.Top="29"
         Width="256"
         Height="191"
         ItemsSource="{StaticResource ItemsData}"
         SelectionChanged="checkListBox_SelectionChanged"
         SelectionMode="Multiple" />
           

接下來說說為什麼我把樣式寫了兩份,首先看看第一個樣式:

<Style TargetType="{x:Type ListBoxItem}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <CheckBox Margin="{TemplateBinding Margin}"
                          Content="{TemplateBinding Content}"
                          ContentTemplate="{TemplateBinding ContentTemplate}"
                          ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}"
                          FocusVisualStyle="{TemplateBinding FocusVisualStyle}"
                          IsChecked="{Binding IsSelected,
                                              RelativeSource={RelativeSource TemplatedParent}}" />
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
           

如果你覺得這份代碼很簡單,那麼可能你WPF所知甚多,也可能你和原來的我一樣,完全不懂得為什麼使用TemplateBinding。

WPF的過于靈活,會讓一個功能的實作有千萬種寫法,很多時候,當你發現一個樣式怎麼都對不上的時候,往往問題已經很難解決。

使用TemplateBinding無非有兩種情況(自我總結):

  1. 普通綁定
  2. 不在模闆中寫死,給外面留下擴充

關于第一點這裡就不說了,如上面的Content,如果這裡沒有這句綁定,那麼你會發現隻有一個複選框,而沒有文字。

關于第二點,這裡有個複用的問題,寫在模闆中,不管外界設定什麼,模闆中的值都是不會改變的。這就無形中将一個功能徹底定死,如果沒有絕對的原因,請千萬别這麼做,為說明,這裡展示第二個樣式:

<Style TargetType="{x:Type ListBox}">
    <Setter Property="ItemContainerStyle">
        <Setter.Value>
            <Style BasedOn="{StaticResource {x:Type ListBoxItem}}" TargetType="{x:Type ListBoxItem}">
                <Setter Property="Margin" Value="2,2,0,0" />
            </Style>
        </Setter.Value>
    </Setter>
</Style>
           

第二個樣式主要的功能其實就是設定ListBoxItem的Margin屬性,但是為什麼當初沒有把這個值直接寫在第一個樣式中。

這時候有人會說,我可以在外面再重寫模闆一樣可以實作,是的,一樣可以,但是為什麼要偏偏這麼說呢,孰優孰劣,一眼便知。

WPF中的實作方案

這隻是為了做對比,使用完全WinForm的事件驅動方式完成。

WPF中當然不必如此,我會在下一節中使用WPF的方式去逐漸實作這個功能,這裡先看看如我這等從WinForm遷移倒WPF中首先出現的問題,找事件。

所辛的是,這個例子中都能找到相應的事件,如果換做别的功能,你可能會發現,很多WinForm中有的事件,在WPF中卻沒有,當問題曝露出來的時候,你應該考慮系統的學學WPF中的實作方式,而不是按照自己的方式繼續去實作。

咱們先看看在WPF中直接使用WinForm的思路去做。

private void checkListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    chbxAll.IsChecked = checkListBox.SelectedItems.Count == 0 ? false :
            checkListBox.SelectedItems.Count == checkListBox.Items.Count ? (bool?)true : null;
}
private void chbxAll_Checked(object sender, RoutedEventArgs e)
{
    checkListBox.SelectAll();
}
private void chbxAll_Unchecked(object sender, RoutedEventArgs e)
{
    checkListBox.UnselectAll();
}
           

你會發現,似乎很簡單,比WinForm中還要簡單。

但是你可知道,WPF如果要做到真正的頁面與邏輯分離,背景是沒有任何邏輯代碼的,要有,也僅僅是一些控制界面的顯示的代碼。

當然,我們不需要把一件事情做的太徹底,而且強求不在背景寫代碼也不是一種正确的開發方式,而例子寫在這裡,也僅僅隻想表達一個意思,那就是在WPF中一個功能的實作方式太多,希望大家不要因為實作了而不再去探索更好的實作方式,有時候這也是對于WPF設計方面的一次學習。

  源碼位址: https://github.com/NanQi/demo/tree/master/SelectDemo