在写DirectUI时有这么一个需求,就是加载一串XML,如何生成一棵对应的控件树?

比如XML如下:

<panel>

       <button id=”1” text=”button”/>

<label id=”2” text=”label”/>

      <panel>

            <label id=”3” text=”good”/>

        </panel>

 

</panel>

如何生成对应的Control Tree?

 

看到这个,我们首先想到的是先对这个控件层次体系进行设计, 并抽象出如下层次:

Class CControlBase

{};

 

Class CButton: public CControlBase

{};

 

Class CLabel: public CControlBase

{};

 

Class CContainer: ControlBase

{};

 

Class CPanel: public CContainer

{};

 

我们可以看到他们的控件类层次如下:

以上层次符合一般面向对象设计的规则,其中CContainer表示控件容器类的基类。

那么如何根据XML创建这些对象呢?

我们想到了抽象工厂(Abstract Factory), 定义如下:

Class CControlFactory

{

Public:

       CButton* CreateButton() { return new CButton;}

       CLabel* CreateLabel() { return new CLabel;}

       CPanel* CreatePanel() { return new CPanel;}

}

 

显然,以上设计每次加入一个新控件都要重新加一个Create方法,不符合面向对象设计的开放封闭原则(OCPOpen Closed Principle), 因此我们把它改成下面的接口:

Class CControlFactory

{

Public:

       CControlBase* CreateControl(const string& strTypeName)

{

              If(strTypeName==”button”)

return new Button;

              Else if(strTypeName==”label”)

return new Label;

              Else if(strTypeName==”panel”)

return new Panel;

              Else

                     Return null;

}

};

 

显然上面的设计尽管比前一个好多了, 但是仍然有硬编码(hard code)的味道, 每次新加一个控件,都要在这里改代码。

 

如何才能新加控件,又不影响这里的代码呢?

我们想到了注册机制, 设计如下:

Class CControlFactory

{

Public:

       Bool RegisterControl(const string& strTypeName, void* lpfnCreateFun);

       CControlBase* CreateControl(const string& strTypeName) ;

 

Protected:

       map<string, void*> m_controlMap;

};

我们看到我们上面的设计是通过注册控件和它的创建方法,保存在一个Map里,然后在CreateControl时通过查询这个Map,调用控件注册的创建方法, 来生成新控件。

可以看到上面的设计已经完全符合开放封闭原则,我们新加控件完全不会影响现有的代码。

 

但是考虑这样一个需求,要求新生成控件要有默认风格,并且这些风格是可变的,比如我们要生成的Button 默认风格是要求以某个图片为背景。

 

显然上面注册创建函数的方法满足不了我们这里的需求,因为我们不可能通过在我们的创建函数里硬编码来指定初始控件风格。

 

怎么样才能让我们新创建的控件有默认风格,并且该默认风格是可变的?

我们想到了设计模式里创建型模式中的原型(Prototype)模式, 给我们的基本控件增加一个Clone方法, 代码如下:

Class CControlBase

{

Public:

       Virtual CControlBase* Clone() { return new CControlBase(*this);}

};

 

Class CButton: public CControlBase

{

Public:

       Virtual CControlBase* Clone() { return new CButton (*this);}

};

 

Class CLabel: public CControlBase

{

Public:

       Virtual CControlBase* Clone() { return new CLabel (*this);}

};

 

Class CContainer: ControlBase

{

Public:

       Virtual CControlBase* Clone() { return new CContainer (*this);}

};

 

Class CPanel: public CContainer

{

Public:

       Virtual CControlBase* Clone() { return new CPanel (*this);}

};

 

而我们ControlFactory的设计如下:

Class CControlFactory

{

Public:

       Bool RegisterControl(const string& strTypeName, CControlBase* pPrototypeControl);

       CControlBase* CreateControl(const string& strTypeName) ;

 

Protected:

       map<string, CControlBase*> m_controlMap;

};

可以看到在注册时我们把控件原型保存起来,然后在CreateControl时通过该控件原型的Clone方法,创建我们的新控件。这种设计下,我们只要在注册时提供不同的原型控件,后面创建时就有不同的默认风格,非常方便。

 

至此,我们基本完成了ControlFactory的工作。

这里还可以优化的是这里的ControlFactory我们可以把它定义成单例(Singleton:

Class CControlFactory

{

Public:

       Static CControlFactory* GetInstacne();

Public:

       Bool RegisterControl(const string& strTypeName, CControlBase* pPrototypeControl);

       CControlBase* CreateControl(const string& strTypeName) ;

      

Protected:

       map<string, CControlBase*> m_controlMap;

};

 

有了Control层次和Control Factory,我们接下来考虑如何解析XML来生成Control Tree?

一般的设计会写一个如下的类:

Class CControlBuilder

{

       Public:

              CControlBase* BuildControlTree(const string& strXML);

};

上面的设计直接通过传入XML,生成Control Tree,看起来很完美。

 

但是显然这里有2件事情要做,一件是XML的解析,另一件是Control Tree的生成。我们把这2个东西柔在一个类里面,对我们以后维护很不方便。

 

比如说我们本来用TinyXML来解析XML的,但是后来发现它是基于DOM的,效率太低,想改用别的XML解析器,这时你就会发现修改CControlBuilder这个类是多么痛苦了。

 

显然,我们比较好的设计是我们应该把XML解析和Build Control Tree2个功能分离开来,这也符合面向对象设计时的单一职责原则(Single Responsibility Principle)

 

我们抽象出一个XML的解析接口:

 

Class IXMLParser

{

       Public:

              Virtual bool SetDoc(const cstring& strXML);

              Virtual string GetTagName();

       Virtual bool FindElem(const string& strName );

       Virtual bool FindChildElem(const string& strName );

       Virtual bool IntoElem();

       Virtual bool OutOfElem();

       Virtual string GetAttrib(const string& strName) const;

       Virtual string GetChildAttrib(const string& strName) const;

};

 

Class CTinyXMLParser: public IXMLParser

{

      

};

 

可以看到通过这种方式,我们把XML的解析过程抽象出来,我们本身不用关心解析器的类型(说明:上面XML解析的设计不一定合理,只是一个示例)

 

现在我们可以开始写我们的Control Tree Builder了,

Class CControlBuilder:

{

       Public:

              CControlBase* BuildControlTree(const string& strXML)

              {

                     CTinyXMLParser parser;

                     parser.SetDoc(strXML);

                     CControlFactory* pFactory = CControlFactory::Instance();

                    

                     String strControl = parser.GetTagName();

                     CControlBase* pRoot = pFactory->CreateContorl(strControl);

                     ….

 

                     Return pRoot;

}

};

 

显然上面的方式, 我们修改XML解析器的类型很不方便,我们可以通过工厂方法(Factory Method)来方便以后扩展。

 

 

Class CControlBuilder

{

       Public:

              CControlBase* BuildControlTree(const string& strXML)

              {

                     Auto_ptr< IXMLParser > parser = CreateXMLParser() ;

                     Parser->SetDoc(strXML);

                     CControlFactory* pFactory = CControlFactory::Instance();

                    

                     String strControl = parser->GetTagName();

                     CControlBase* pRoot = pFactory->CreateContorl(strControl);

                     ….

 

                     Return pRoot;

}

 

Protected:

Virtual IXMLParser* CreateXMLParser()

{

       Return new CTinyXMLParser;

}

};

 

好了,到这里我们所有的设计都全部完成了,基本类图如下:

PS, 在设计CControlBuilder时,本来考虑是不是该用设计模式中的生成器(builder)模式, 但是Builder模式强调的是同样的创建过程,生成不同的产品。显然我们这里无论XML如何解析,最终只有Root Control这一种产品。 所以这里如果用这个模式的话,就有点过度设计了。

 

总结一下,我们上面用了哪些设计模式?
Composite, Abstract Factory, Factory Method, Singleton, Prototype.
你看出来了吗?
你们有更好的设计思路吗?

 说明: (1)上面的代码都只是设计时的伪代码,拿来编译肯定过不了
        (2)考虑Windows的窗口类注册和创建机制,会发现尽管WindowsAPIC语言方式,但是设计思想是类似的。
        (3) 对于DirectUI来说,上面控件类的这种层次设计其实很滥, 有兴趣的话可以学下WPFJ

 

 

posted on 2012-06-10 17:31 Richard Wei 阅读(4240) 评论(4)  编辑 收藏 引用 所属分类: 设计模式

FeedBack:
# re: 生成DirectUI 控件树的设计过程
2012-06-11 16:38 | 饭中淹
我觉得有几个问题
1- UI依赖了XML。因为你如果要改变数据源,你需要改变UI内部的东西。
我做的系统里,用了抽象的DOM数据NODE的概念。XML只是建立数据NODE的一个源而已。

2- UI的创建很多都用不到。比如prototype这种,如果你想让你的UI从数据上创建,那么你就用一个从数据创建就好了。

我的系统里,UI控件的CREATE都是只有统一的一个参数,IDataNode。这样接口非常简单。

  回复  更多评论
  
# re: 生成DirectUI 控件树的设计过程
2012-06-11 18:12 | Richard Wei
@饭中淹

没太看明白你说的问题, 但是我想对于UI来说很多东西是类似的,
就一个UI(窗口)本身是一棵递归的树,内部有容器和控件,相当于树的分支(node)和树叶(leaf),并且可以无限递归, 这个层次很符合Composite模式,也很符合用XML来描述。

所以现在比较流行的UI框架(比如WPF, WEB页面)都分2层来描述,一层是标记(markup). 比如WPF里的XAML, Web页面里的HTML; 另外一层是代码(code), 比如WPF里用C#, Web页面里用javascript.

其实本质上code部分才是真正的实现,内部定义了所有的控件层次, 所以即使不依赖markup部分,我们同样可以定义和操作所有的UI, 但是借助Markup可以让我们更方便的生成和修改UI。

至于用不用Prototype模式不太重要,我这里的想法是让控件有默认的风格属性,而且这个默认风格属性也是容易修改的。通过用Prototype,我可以在XML里定义默认的控件原型,程序初始化时用该原型来注册控件就可以了。另外, 如果有些控件你觉得用不到,不去注册就好了。 这种注册机制, 对于支持插件(控件)也很方便,你觉得呢?  回复  更多评论
  
# re: 生成DirectUI 控件树的设计过程
2012-06-12 14:09 | 饭中淹
。。。。并不是说用XML不好,或者否定UI树什么的。

我也是用的MARKUP来建立UI控件树,并绑定代码。不过,我是用了一个中间的DOM抽象数据层。
这样,不管外面有什么XML,HTML甚至INI,我只要一个数据抽象层放进UI里面就好了。





@Richard Wei
  回复  更多评论
  
# re: 生成DirectUI 控件树的设计过程
2012-06-26 15:46 | 叫我老王吧
最近也在写UI,受益匪浅,受教了  回复  更多评论
  

只有注册用户登录后才能发表评论。
【推荐】超50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理