StupidBeauty
Read times:2669Posted at:Sun Jun 18 04:26:12 2017 - no title specified

《C++面向对象软件设计及构建》文章翻译:3.6更复杂的关联,3.6 More Complex Associations

真实的面向对象系统中,会牵涉到来自多个类的多个对象之间的关联。商用软件系统中,可能会牵涉到数以十计的类,以及成百上千的对象。此处我们要讲的示例,倒不会达到这个数量级。然而,会使用同样的技巧来引入多个额外的类,以展示,这样的复杂的、真实的系统会是如何构成的。

我们会制作出一个使用了关联功能的真实版本的Frame 类。关联,是有必要的,因为,如果将窗口中的所有丰富功能都直接完整地放置在这个接口中的话,Frame 类就会变得极端复杂,复杂到不可忍受。例如,目前仅可以绘制两种形状(线段和圆)。但是,还有狠多形状也是常见的,包括椭圆、矩形、曲线、和多边形,并且还可能为每种形状定义多种属性,例如颜色、线条宽度、线条模式、填充模式。显然,如果打算将所有这些细节都通过接口来进行控制的话,会创造出一个极端冗长且复杂的接口。另外,Frame类还需要进行扩展,以加上多种交互元素,使得用户能够对界面进行操作。这些交互元素包括:按钮、可编辑的文本框、滑动条、复选框、可滚动的列表、单选框,以及其它元素。即使只是向类中为每种交互元素加入一到两个方法,都显然会搞出一个臃肿的、无法控制的接口。

窗口中,图形方面和交互方面的职责,可分配到三种关联的类上去:

  • •. Frame:显示屏幕上的一个矩形区域,可移动,可改变大小。

  • •. Canvas:Frame 中的某个区域,用于绘制文字和图形内容,并且对鼠标移动进行响应

  • •. Panel:Frame 中的某个区域,其中包含着交互式元素。

Frame类已经简化,使得,其中仅保留着那些并未放置到Canvas 和Panel 类中去的职责。Canvas类覆盖了所有与绘制功能相关的职责。Canvas 区域中的鼠标事件,如今是关联到Canvas,而不再关联到Frame。Panel类,覆盖了所有与管理交互元素相关的职责。

调整过的Frame 类,以及新的Canvas 类,其定义如下所示。日后再说明Panel 类。

Frame(版本4)

class Frame {                          // 版本4

     private:  

          // 封装起来的实现,放置在这里

     public:

           Frame(char* name, Location p, Shape s);      // 精确描述

           Frame(char* name, Shape s, Location p);      // 精确描述

           Frame(char* name, Location p);               // 默认形状

           Frame(char* name, Shape s);                  // 默认位置

           Frame(char* name );                          // 仅指定名字

           Frame();                                     // 全部使用默认值;

     int   IsNamed(char* aName);                // 妳是否叫这个名字?

     void  MoveTo(Location newLocation);        // 移动窗口

     void  Resize(Shape    newShape);           // 改变形状

     void  Resize(float factor);                // 增大/减小指定倍数

          ~Frame();

   };

注意,与绘制相关的方法(例如,DrawText、DrawLine),都从Frame 类中去除了,这些绘制方法都放置到新定义的Canvas 类中了。Frame 类中剩下的那些方法,都是用来处理Frame 本身的定义和管理的。这样,Frame类就表示的是,用户屏幕上的一个有边界的区域,该区域可按照代码的控制来改变位置和形状。Frame 的内容呢,是由其它的类(例如Canvas和Panel类)来向Frame 中添加的。

Canvas

class Canvas {

     private:  

          // 封装的实现位于这里

     public:

           Canvas(Frame& fr, char* nm, Location loc, Shape sh);

     int   IsNamed(char* aName);                // 这是妳的名字吗?

     void  DrawText(char *text, Location loc);  // 显示文字内容

     Shape TextSize(char *msg);                 // 计算字符串的形状

     void  DrawLine(Location p1, Location p2);  // 绘制线段

     void  DrawCircle(Location center,

                      int radius);              // 绘制

     void  Clear();                             // 擦除整个Frame 的内容

     void  Clear(Location corner,

                 Shape rectangle);              // 擦除矩形区域的内容

          ~Frame();

   };

注意,用于绘制文字内容和图形形状的方法,移动到了Canvas 类中。另外也要注意,Canvas 对象,只能通过与单个Frame 关联的方式来构造。因此,不可能让同一个Canvas 同时位于两个不同的Frame之中,也不可能创建一个不与任何Frame 关联的Canvas 对象。

Canvas 对象与Frame 对象之间的关联,是由Canvas 的构造函数来建立的。与之相对的是,在其 它示例中,两个对象之间的关联是由某个方法来创建的,而不是由构造函数来创建(例如,Counter 和Clock 类中的ConnectTo 方法)。 构造函数,可用来建立一个稳固的关联关系(在Canvas 对象创建时创建该关联关系,并且在该个Canvas 对象的余生中不再改变)。在其它情形下,关联关系可能会更具动态性。例如,某个Message 对象,在它生命过程中的不同时刻,可能会被显示到不同的Frame中。动态关联关系,以非构造函数的方法的形式来表达就更自然,这样,在那些对象的生命过程中,可以调用这些方法来改变其关联关系。

此处会引入三个额外的类,它们用来定义交互式控制元素。利用这些新的类的对象,可以创建出一套交互性更强的界面。这三个类是:

  • •. Panel:之前已经提到过,它是位于Frame 中的一个区域,其中包含着交互式元素,例如Button 和TextBox 对象。

  • •. Button:它所抽象的是,一个简单的、可按动的、带有名字的按钮,用户在该个按钮的显示图片界线之内点击,就可以达到“按下”效果。按钮,显示出来的效果是,一个包含了该按钮的名字的矩形。

  • •. TextBox:提供了一种机制,使得用户能够编辑一砣文字内容,该文字内容日后可被程序读取。

这三个类,组合到一起,就提供了一套基本的控件,可让用户输入数据,以及触发由程序所进行的动作。

Panel 类的定义,如下所示。与Canvas 类类似,Panel 对象的构造函数中,也需要一个Frame 对象作为参数。Panel 在其所关联的Frame 中的位置和形状,也必须传入到构造函数中。通过重载的Add 方法,能够让任意数量的Button和TextBox出现在Panel 中。

Panel

class Panel {

 private:

        // 隐藏的数据

 public:

   Panel(Frame& fr, char *nm, Location loc, Shape sh);

   char* getName();

   void Add(Button& button);

   void Add(TextBox& tbox);

  ~Panel();

 };

Button 类的定义,如下所示。 Button 类的构造函数中,要求给出Button 对象的名字。名字 ,有 两个用途。首先 ,按钮的名字,会出现在屏幕上,显示在按钮(Button)的图形区域的文本标签中。因此 ,一个名为"Start"的Button 对象, 其显示出来的效果是, 一个带边框的矩形框,围绕在文字 Start 的周围。其次 ,当用户“按下”这个 Button( 也就是说,用户点击 了对应 着该个Button 的带边框矩形框 ) 的时候,函数OnPush(char*) 会被调用, 该个Button 的名字就会作为参数被传入。 OnPush(char*) 是一个新函数,它会被加入到 这个简单的编程环境中。 这样,程序 就能够利用OnPush 函数的参数和Button 对象的IsNamed 方法来确定,用户究竟点击 了多个Button中的哪一个。

Button

class Button {

 private:

                // 隐藏的实现

 public:

   Button(char* name, Location loc, Shape sh);

   int IsNamed(char* name);

 ~Button();

 };

下面所示TextBox类,允许用户编辑和/或输入数据。每个TextBox,其显示出来的效果是,一个带边框的矩形区域,当鼠标指针被移动到TextBox 区域内部时,就会有一个文本输入光标出现。当这个光标可见时,用户就能够在TextBox 中编辑、删除或加入任意可见文字内容。TextBox会将多行文字滚动显示,因此,在任何时候,可能只有一部分文字是可见的。程序可使用 TextBox的SetText 和GetText 方法来设置或查询该TextBox 的当前值。

TextBox在构造的时候,可以传入一个可选的文字标签内容,该个文字标签会出现在TextBox 的左侧。TextBox 的形状(Shape),必须足够宽,使得,它能够容纳下文字标签以及用户可能输入的文字内容的长度。

TextBox

class TextBox {

private:

                // 隐藏的实现

public:

        TextBox( Location p, Shape s, char* label);

        TextBox( Location p, Shape s);

        TextBox( char* label);

        TextBox();

       ~TextBox();

  char* GetText();

  void  SetText(char* val);

};

下面展示 了一个小巧的系统,它使用了所有的那些新的类。 这个系统中,向用户显示了一个TextBox,用户可在其中输入一个字符串,并且 ,当用户点击了一个显示着 Copy 字样的按钮时,TextBox 中的当前内容会被程序读取并显示到某个Canvas 区域中。注意 ,Button 和TextBox 都是位于( 与之关联 )一个Panel 中,并且 ,该个Panel 和Canvas 都是位于( 与之关联 )一个Frame 对象中。另外 ,注意,在OnPush 方法中,利用Button 类的IsNamed 方法来识别究竟 是哪个Button 对象被按下了。

一个示例系统

Frame    window ("TestWindow", Location(100,100), Shape(500, 300));

Canvas   canvas (window, "DrawAreas",  Location(1, 1),  Shape(100, 100));

Panel    panel  (window, "Controls", Location(150, 10), Shape(300, 100));

Button   button ("Copy", Location(5, 5), Shape(50,30));

TextBox tbox    (Location(5,50), Shape(150,30), "Enter:");

char    *text;

void OnStart()        // 当"Start"按钮被按下时,会调用一次

{  canvas.Clear();

   panel.Add(button);

   panel.Add(tbox);

   text = (char*)0;

}

void OnPush(char *buttonLabel)

{  if (button.IsNamed(buttonLabel))

   {   canvas.Clear();

       canvas.DrawText(tbox.GetText(), Location(20, 20));

       text = tbox.GetText();

   }

}

void OnPaint()

{ canvas.DrawText(text, Location(20,20));

}

Clock类,可被扩展,以提升Clock 类的可用性。程序猿可能会需要使用多个不同的Clock(例如,为了对不同的事件进行时序控制,为了使用不同的时间间隔),同时,程序猿需要能够灵活地定义,当某个Clock 触发了时间事件时,应当做什么动作。为了达到这个灵活性,就需要按照如下的方式对Clock 类进行扩展。每个Clock对象,在构造时,都会指定一个名字,以用来唯一地标识该个Clock 对象。Clock的时间间隔,可通过构造函数来指定,或者,当Clock 处于停止状态时,可通过SetInterval 方法来指定。Clock,可通过Start 和Stop 方法来控制。最后,与Button 对象类似,在Clock 对象中,有一个IsNamed 方法,它的作用是,将该个对象的名字与传入的字符串参数做比较。注意,在之前的定义中,Clock 可以连接到一个Counter 对象。在每个时间间隔的结束时刻,Clock对象或者会调用它所连接到的Counter 对象的Next()方法,或者,如果它没有连接到Counter 对象的话,则会调用OnTimerEvent()函数,如下所示。

修改过的Clock

class Clock {

private:

                // 隐藏的实现

public:

  Clock (char* name, int interval=1000);

  void SetInterval(int newInterval);

  void Start();

  void Stop();

  int  IsNamed(char* name);

  ~Clock();

};

简单编程环境中的一个扩展,允许程序猿定义,当某个特定的Clock 触发了一个定时器事件时,该做出什么动作。OnTimerEvent函数被重新定义,现在会传入一个字符串参数,它即是触发了此次定时器事件的Clock 的名字。

由于Frame 类中的变动的影响,Message 类也必须做些微小的修改。如今,Message 对象不再直接显示在Frame 中了;而是显示在Canvas 中。这个微小的变化,如下所示。

修改过的Message 类

class Message {

private:

                //封装过的实现

public:

    ...

    void  DisplayIn (Canvas&   whichCanvas);

    ...

};

对于Clock 对象、修改过的Message 类的对象以及修改过的OnTimerEvent 函数的使用,在以下示例中展示。这个程序,是由之前的Blinking Text Hello World 程序修改而来。在这个程序中,会使用一个Clock 对象来控制Message 对象中包含的文字内容的闪烁。Clock的时间间隔,已在构造函数中定义为500 毫秒。Clock是在OnStart 方法中启动的,它的定时器事件,是由OnTimerEvent 方法所响应,该方法会将通过参数传递进来的名字与Clock 对象本身的名字进行比较。

使用Clock的示例

Frame   window   ("Message Test", Location(100,100), Shape(200,200));

Canvas  canvas   (window, "Message Area", Location(10,10), Shape(180,180));

Message greeting ("Hello World!", Location(20,20));

Clock   timer    ("timer", 500);

int     onoff;

void OnStart()

{  greeting.DisplayIn(canvas);

   greeting.Draw();

   onoff = 1;

   timer.Start();

};

void OnTimerEvent(char* clockName)

{

   if( timer.IsNamed(clockName) )

   {   if (onoff) { greeting.Clear(); onoff = 0; }

       else       { greeting.Draw();  onoff = 1; }

   }

}

void OnPaint()

{ if (onoff) greeting.Draw();

}

1 任务

  1. 1.使用Counter、Message 和Frame 类来编写一个程序,它会实现一个简单的计时器。使用OnTimerEvent 函数来更新Counter 对象。使用鼠标左键的点击事件来控制OnTimerEvent 当前是否要更新Counter 对象。第一次鼠标左键点击事件会“开始”计时(后续OnTimerEvent函数运行过程会更新Counter 对象,具体是通过Counter 对象的Next 方法来实现)。第二次鼠标左键点击事件会“停止”计时。也就是说,连续点击鼠标左键会轮流地开始及停止计时。

  2. 2. 按钮计时器。使用Clock、Counter、 Message、Button 、Frame 和其它类来实现一个程序,它是带有一个按钮的计时器。使用Clock 对象来触发对于OnTimerEvent 函数的调用,该函数会更新Counter 对象。使用一个显示着"Control"的Button 对象来控制,OnTimerEvent 函数当前是否要更新Counter 对象。第一次按下这个Button,就会“开始”计时(后续OnTimerEvent函数运行过程会更新Counter 对象,具体是通过Counter 对象的Next 方法来实现 )。第二次按下这个Button,就会“停止”计时。也就是说,连续按下这个Button,就会轮流地开始及停止计时。

  3. 3.构造一个系统,其中显示三个Message,每个Message 会显示某个不同的Counter 中的值。与"Tens"所关联的那个Counter,应当在与"Units"所关联的计数器被增加10次时增加一次;类似地,对于"Hundreds"和"Tens"也是这样的关系。使用另外三个Message,用于标识"Units"、"Tens"和"Hundreds"。

  4. 4.构造一个系统,其中显示着一个一秒钟的Clock,当用户点击屏幕上不同的按钮时,可分别开始及停止该定时器。

  5. 5. 构造 一个系统,其中有两个Clock。 一个的时间间隔是1秒,另一个的时间间隔是0.1 秒。每个Clock 都连接到各个不同的计时器,并且 可被各自独立的开始( start )和停止( stop )按钮控制。两个计时 器,都会显示在屏幕上,并且带有适当 的文字标签。

  6. 6.构造一个系统,其中有一个一秒钟的Clock,它显示在屏幕上,可被开始、停止及重置。要想重置这个Clock 的话,用户必须先停止这个Clock,在TextBox 中输入一个新的时间,按下重置(reset)按钮,再按下开始(start)按钮。

未知美人

孟茜

Your opinions
Your name:Email:Website url:Opinion content: