趁此机会做个广告,http://www.gaclib.net终于上线啦!
GacUI的列表控件的第二个Demo是关于列表项的多选的。跟Windows一样,我们可以通过鼠标和方向键,配合CTRL和SHIFT选择列表的多个内容。因此这次我实现了一个简单的“名字选择窗口”,就跟QQ邀请好友入群的界面一样,两个列表,两个按钮。先看图:
列表内容始终是排序的。当我们选中一边的内容之后,可以按按钮把内容复制到另一边。现在我们先来看一看创建和排版这些控件的代码。这里我用了一个五行三列的表格。左右方列表,中间的第二个第四行放按钮,第三行64个像素高,按钮32*32,第一行和第五行平均地充满剩下的空间:
#include < Windows.h >
// for SortedList, CopyFrom and Select
using namespace vl::collections;
int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int CmdShow)
{
return SetupWindowsDirect2DRenderer();
}
class NameSelectorWindow : public GuiWindow
{
private :
GuiTextList * listSource;
GuiTextList * listDestination;
GuiButton * buttonAdd;
GuiButton * buttonRemove;
(略)
public :
NameSelectorWindow()
:GuiWindow(GetCurrentTheme() -> CreateWindowStyle())
{
this -> SetText(L " Controls.ListBox.NameSelector " );
GuiTableComposition * table = new GuiTableComposition;
table -> SetRowsAndColumns( 5 , 3 );
table -> SetCellPadding( 3 );
table -> SetAlignmentToParent(Margin( 0 , 0 , 0 , 0 ));
table -> SetRowOption( 0 , GuiCellOption::PercentageOption( 0.5 ));
table -> SetRowOption( 1 , GuiCellOption::MinSizeOption());
table -> SetRowOption( 2 , GuiCellOption::AbsoluteOption( 64 ));
table -> SetRowOption( 3 , GuiCellOption::MinSizeOption());
table -> SetRowOption( 4 , GuiCellOption::PercentageOption( 0.5 ));
table -> SetColumnOption( 0 , GuiCellOption::PercentageOption( 0.5 ));
table -> SetColumnOption( 1 , GuiCellOption::MinSizeOption());
table -> SetColumnOption( 2 , GuiCellOption::PercentageOption( 0.5 ));
this -> GetContainerComposition() -> AddChild(table);
{
GuiCellComposition * cell = new GuiCellComposition;
table -> AddChild(cell);
cell -> SetSite( 0 , 0 , 5 , 1 );
listSource = g::NewTextList();
listSource -> GetBoundsComposition() -> SetAlignmentToParent(Margin( 0 , 0 , 0 , 0 ));
// make listSource's horizontal scroll bar disappeared when it is not needed.
listSource -> SetHorizontalAlwaysVisible( false );
listSource -> SetMultiSelect( true );
listSource -> SelectionChanged.AttachMethod( this , & NameSelectorWindow::listSource_SelectionChanged);
cell -> AddChild(listSource -> GetBoundsComposition());
}
{
GuiCellComposition * cell = new GuiCellComposition;
table -> AddChild(cell);
cell -> SetSite( 0 , 2 , 5 , 1 );
listDestination = g::NewTextList();
listDestination -> GetBoundsComposition() -> SetAlignmentToParent(Margin( 0 , 0 , 0 , 0 ));
// make listSource's horizontal scroll bar disappeared when it is not needed.
listDestination -> SetHorizontalAlwaysVisible( false );
listDestination -> SetMultiSelect( true );
listDestination -> SelectionChanged.AttachMethod( this , & NameSelectorWindow::listDestination_SelectionChanged);
cell -> AddChild(listDestination -> GetBoundsComposition());
}
{
GuiCellComposition * cell = new GuiCellComposition;
table -> AddChild(cell);
cell -> SetSite( 1 , 1 , 1 , 1 );
buttonAdd = g::NewButton();
buttonAdd -> SetText(L " >> " );
buttonAdd -> GetBoundsComposition() -> SetAlignmentToParent(Margin( 0 , 0 , 0 , 0 ));
buttonAdd -> GetBoundsComposition() -> SetPreferredMinSize(Size( 32 , 32 ));
buttonAdd -> Clicked.AttachMethod( this , & NameSelectorWindow::buttonAdd_Clicked);
buttonAdd -> SetEnabled( false );
cell -> AddChild(buttonAdd -> GetBoundsComposition());
}
{
GuiCellComposition * cell = new GuiCellComposition;
table -> AddChild(cell);
cell -> SetSite( 3 , 1 , 1 , 1 );
buttonRemove = g::NewButton();
buttonRemove -> SetText(L " << " );
buttonRemove -> GetBoundsComposition() -> SetAlignmentToParent(Margin( 0 , 0 , 0 , 0 ));
buttonRemove -> GetBoundsComposition() -> SetPreferredMinSize(Size( 32 , 32 ));
buttonRemove -> Clicked.AttachMethod( this , & NameSelectorWindow::buttonRemove_Clicked);
buttonRemove -> SetEnabled( false );
cell -> AddChild(buttonRemove -> GetBoundsComposition());
}
// Add names into listSource
LoadNames(listSource);
// set the preferred minimum client size
this -> GetBoundsComposition() -> SetPreferredMinSize(Size( 640 , 480 ));
// call this to calculate the size immediately if any indirect content in the table changes
// so that the window can calcaulte its correct size before calling the MoveToScreenCenter()
this -> ForceCalculateSizeImmediately();
// move to the screen center
this -> MoveToScreenCenter();
}
};
void GuiMain()
{
GuiWindow * window = new NameSelectorWindow;
GetApplication() -> Run(window);
delete window;
}
剩下的内容分为三部分。首先是如何在列表没有选中内容的时候把按钮变灰。在上面的代码中我们知道listSource和listDestination都监听了SelectionChanged事件。事件处理函数的内容如下:
{
buttonAdd -> SetEnabled(listSource -> GetSelectedItems().Count() > 0 );
}
void listDestination_SelectionChanged(GuiGraphicsComposition * sender, GuiEventArgs & arguments)
{
buttonRemove -> SetEnabled(listDestination -> GetSelectedItems().Count() > 0 );
}
代码内容十分简洁明了。第二部分是NameSelectorWindow构造函数中调用的LoadNames函数。我们首先把所有的名字都放在一个const wchar_t* DataSource[]数组里面,内容是没有排过序的:
{
L " Weierstrass " ,
L " Cantor " ,
L " Bernoulli " ,
L " Fatou " ,
L " Green " ,
L " S.Lie " ,
L " Euler " ,
L " Gauss " ,
L " Sturm " ,
L " Riemann " ,
L " Neumann " ,
L " Caratheodory " ,
L " Newton " ,
L " Jordan " ,
L " Laplace " ,
L " Wiener " ,
L " Thales " ,
L " Maxwell " ,
L " Riesz " ,
L " Fourier " ,
L " Noether " ,
L " Kepler " ,
L " Kolmogorov " ,
L " Borel " ,
L " Sobolev " ,
L " Dirchlet " ,
L " Lebesgue " ,
L " Leibniz " ,
L " Abel " ,
L " Lagrange " ,
L " Ramanujan " ,
L " Ljapunov " ,
L " Holder " ,
L " Poisson " ,
L " Nikodym " ,
L " H.Hopf " ,
L " Pythagoras " ,
L " Baire " ,
L " Haar " ,
L " Fermat " ,
L " Kronecker " ,
L " E.Laudau " ,
L " Markov " ,
L " Wronski " ,
L " Zermelo " ,
L " Rouche " ,
L " Taylor " ,
L " Urysohn " ,
L " Frechet " ,
L " Picard " ,
L " Schauder " ,
L " Lipschiz " ,
L " Liouville " ,
L " Lindelof " ,
L " de Moivre " ,
L " Klein " ,
L " Bessel " ,
L " Euclid " ,
L " Kummer " ,
L " Ascoli " ,
L " Chebyschev " ,
L " Banach " ,
L " Hilbert " ,
L " Minkowski " ,
L " Hamilton " ,
L " Poincare " ,
L " Peano " ,
L " Zorn " ,
};
然后LoadNames函数如下所示:
{
// Use linq for C++ to create sorted TextItem(s) from DataSource
CopyFrom(
list -> GetItems(),
FromArray(DataSource)
>> OrderBy(_wcsicmp)
>> Select < const wchar_t * , list::TextItem > (
[]( const wchar_t * name){ return list::TextItem(name);}
)
);
}
首先我们用FromArray函数从const wchar_t* DataSource[]数组创建出一个IEnumerable<const wchar_t*>,然后用_wcsicmp进行排序,最后把每一个const wchar_t* name都用list::TextItem(name)转变成一个列表项。
最后一步比较复杂,就是如何在移动列表的内容的时候还保持两个列表的内容是排序的。首先,从一个排序列表中删除东西,结果肯定是排序的。其次,把一堆新的名字插入一个列表,最简单的方法就是把所有的东西连起来重新排序一遍,然后放回去:
{
return list -> GetItems()[index];
}
static int CompareTextItem(list::TextItem a, list::TextItem b)
{
return _wcsicmp(a.GetText().Buffer(), b.GetText().Buffer());
}
static int ReverseCompareInt( int a, int b)
{
return b - a;
}
void MoveNames(GuiTextList * from, GuiTextList * to)
{
CopyFrom(
to -> GetItems(),
to -> GetItems()
>> Concat(
from -> GetSelectedItems() >> Select(Curry(GetTextItem)(from))
)
>> OrderBy(CompareTextItem)
);
List < int > selectedItems;
CopyFrom(
selectedItems.Wrap(),
from -> GetSelectedItems()
>> OrderBy(ReverseCompareInt)
);
FOREACH( int , index, selectedItems.Wrap())
{
from -> GetItems().RemoveAt(index);
}
}
void buttonAdd_Clicked(GuiGraphicsComposition * sender, GuiEventArgs & arguments)
{
MoveNames(listSource, listDestination);
}
void buttonRemove_Clicked(GuiGraphicsComposition * sender, GuiEventArgs & arguments)
{
MoveNames(listDestination, listSource);
}
我们可以看到MoveNames函数里面做了三件事情。
第一件事情就是把to列表中的内容,和from列表中选中的内容Concat起来,也就是一个连着一个组成一个没有排序过的IEnumerable<list::TextItem>,然后用CompareTextItem进行排序。其中Curry(GetTextItem)(item)的意思是,将item作为GetTextItem的第一个参数,把“填补剩下的参数”的这个过程产生一个新的函数,就跟bind1st函数的意思是一样的。所以经过这个Select,就可以吧GetSelectedItems()返回的所有选中的item的下标,给转换成原原本本的列表项。这样将to里面的列表项和from里面选中的列表项所有的内容合起来排序,最后放回to列表里面,这样东西就挪过去了。
第二件事情就是要把from->GetSelectedItems()的内容都复制下来,然后逆序。
第三件事情,就是遍历逆序后的GetSelectedItems的结果,一个一个删除掉选中的项目了。列表控件只要内容一变化,选中就会全部清空,因此如果不先保留GetSelectedItems的结果的话,删除了一个列表项之后,GetSelectedItems就会变空,这样就会出bug。
这个Demo就介绍到这里了。GacUI下一个列表控件的Demo将是关于虚拟模式的。也就是说,这次我们不一个一个Add进去了,而是直接告诉列表控件一共有多少个项目,然后列表要显示的时候回调一下获得真正的内容。这个功能是很有用的,特别是当内容特别多,我们没办法一下子准备好的时候,可以让界面很快的出来,让用户感觉起来,程序运行的很流畅。