In this chapter, we will explore the design of the graphical user interface (GUI) for pdCalc. Whenever one designs a GUI, a widget platform needs to be selected. As previously noted, I have chosen to use Qt for the creation of the GUI. That said, this is not a how-to chapter on using Qt to design an interface. Rather, I assume that the reader has a working knowledge of Qt, and the chapter itself focuses on design aspects of the GUI. In fact, as much as possible, I will refer the reader to the source code to see detailed aspects of the widget implementations. Any discussion of the Qt implementation is either merely incidental or worthy of particular emphasis. If you have no interest in GUI design, this chapter can be skipped entirely with virtually no loss in continuity.
在本章中,我们将探讨pdCalc的图形用户界面(GUI)的设计。每当人们设计图形用户界面时,都需要选择一个部件平台。如前所述,我选择使用Qt来创建图形用户界面。也就是说,这不是一个关于如何使用Qt来设计界面的章节。相反,我假设读者对Qt有一定的了解,而这一章本身的重点是GUI的设计方面。事实上,我将尽可能地把读者引向源代码,以看到部件实现的详细方面。对Qt实现的任何讨论要么只是附带的,要么值得特别强调。如果你对GUI设计没有兴趣,这一章可以完全跳过,几乎没有任何连续性的损失。
6.1 Requirements(要求)
In Chapter 5, we began our analysis of the command line interface (CLI) by deriving an interface abstraction that would be used by both the CLI and the GUI. Obviously, we will reuse this interface here, and we therefore already know the abstract interface to which our overall user interface must conform. We thus begin this chapter by defining the requirements for the GUI specialization.
在第五章中,我们开始分析命令行界面(CLI),推导出CLI和GUI都会使用的接口抽象。很明显,我们将在这里重新使用这个接口,因此我们已经知道了我们的整个用户界面必须符合的抽象接口。因此,我们在本章开始时定义了对GUI专用化的要求。
As with the CLI, we quickly discover that the requirements from Chapter 1 are woefully inadequate for specifying a graphical user interface. The given requirements are only functional. That is, we know what buttons and operations the calculator should support, but we know nothing about the expected appearance.
和CLI一样,我们很快发现第一章的需求对于指定一个图形用户界面来说是非常不够的。给出的需求只是功能性的。也就是说,我们知道计算器应该支持哪些按钮和操作,但我们对预期的外观一无所知。
In a commercial project, one would (hopefully) engage the client, a graphic artist, and a human computer interactions expert to assist in designing the GUI. For our case study, it suffices to fully specify our own requirements:
- The GUI should have a window that displays both input and output. The output is the top six entries of the current stack.
- The GUI should have clickable buttons for entering numbers and all supported commands.
- The GUI should have a status display area for displaying error messages.
在一个商业项目中,人们会(希望)让客户、图形艺术家和人机交互专家来协助设计图形用户界面。对于我们的案例研究,只需充分说明我们自己的要求即可:
- GUI应该有一个窗口,显示输入和输出。输出是当前Stack的前六个条目。
- GUI应该有可点击的按钮来输入数字和所有支持的命令。
- GUI应该有一个状态显示区,用于显示错误信息。
The above requirements still do not explain what the calculator should actually look like. For that, we need a picture. Figure 6-1 shows the working calculator as it appears on my Linux desktop (Kubuntu 16.10 using Qt 5.7.1). To show the finished GUI as a prototype for designing the GUI is most certainly “cheating.” Hopefully, this shortcut does not detract from the realism of the case study too much. Obviously, one would not have a finished product at this stage in the development. In a production setting, one might have mock-ups drawn either by hand or with a program such as Microsoft PowerPoint, Adobe Illustrator, Inkscape, etc. Alternatively, maybe the GUI is being modeled from a physical object, and the designer either has photographs or direct access to that object. For example, one might be designing a GUI to replace a physical control system, and the requirements specify that the interface must display identical dials and gauges (to reduce operator retraining costs).
上面的要求仍然没有说明计算器的实际样子。为此,我们需要一张图片。图6-1显示了工作中的计算器,它出现在我的Linux桌面上(Kubuntu 16.10,使用Qt 5.7.1)。把完成的GUI作为设计GUI的原型来展示,无疑是 “作弊”。希望这个捷径不会过多地减损案例研究的真实性。很明显,在开发的这个阶段,人们不会有一个成品。在生产环境中,人们可能会用手或用诸如Microsoft PowerPoint、Adobe Illustrator、Inkscape等程序来绘制模拟图。另外,也许GUI是根据一个物理对象建模的,设计者要么有照片,要么能直接接触到该对象。例如,一个人可能正在设计一个GUI来取代一个物理控制系统,并且要求界面必须显示相同的表盘和仪表(以减少操作员的再培训成本)。
Figure 6-1. The GUI on Linux with no plugins
The GUI for pdCalc was inspired by my HP48S calculator. For those familiar with any of the Hewlett-Packard calculators in this series, the interface will feel somewhat familiar. For those not familiar with this series of calculators (likely, the majority of readers), the following description explains the basic behavior of the GUI.
pdCalc的图形用户界面是受我的HP48S计算器的启发。对于那些熟悉这个系列的惠普计算器的人来说,这个界面会感到有些熟悉。对于那些不熟悉这个系列的计算器的人(可能是大多数读者),下面的描述解释了图形用户界面的基本行为。
The top third of the GUI is a dedicated input/output (I/O) window. The I/O window displays labels for the top six stack levels on the left, with the top of the stack being at the bottom of the window. Values on the stack appear on the right side of the window on the line corresponding to the number’s location on the stack. As the user enters a number, the stack reduces to showing only the top five stack elements, while the number being entered is displayed left justified on the bottom line. A number is terminated and entered onto the stack by pressing the enter button.
GUI的前三分之一是一个专门的输入/输出(I/O)窗口。I/O窗口左边显示前六层Stack的标签,Stack的顶部在窗口的底部。Stack中的数值显示在窗口的右侧,与数字在Stack中的位置相对应的一行。当用户输入一个数字时,Stack减少到只显示Stack的前五个元素,而正在输入的数字则显示在最下面一行的左对齐位置。一个数字被终止,并通过按回车键输入到Stack中。
Assuming sufficient input, an operation takes place as soon as the button is pressed. If insufficient input is present, an error message is displayed above the I/O window. With respect to commands, a valid number in the input area is treated as the top number on the stack. That is, applying an operation while entering a number is equivalent to pressing Enter and then applying the operation.
假设有足够的输入,只要按下按钮,就会有操作发生。如果输入不足,则在I/O窗口上方显示一条错误信息。关于命令,输入区的有效数字被视为堆栈中的最高数字。也就是说,在输入一个数字的同时应用一个操作,相当于按下回车键,然后应用该操作。
To economize on space, some buttons have a shifted operation above and to the left of the button itself. These shifted operations can be activated by first pressing the shift button and then pressing the button below the shifted text. Pressing the shift button places the calculator in shift mode until a button with a shifted operation is pressed or until the shift button is pressed again. For clarity, a shifted operation is often the inverse of the operation on the button.
为了节省空间,有些按钮在按钮本身的上方和左侧有一个移位操作。这些移位操作可以通过先按移位按钮,再按移位文字下面的按钮来激活。按下移位按钮后,计算器将处于移位模式,直到有移位操作的按钮被按下或再次按下移位按钮为止。为了清楚起见,移位的操作往往是按钮上操作的逆向。
To ease input, many buttons are bound to keyboard shortcuts. Numbers are activated by pressing the corresponding number key, the enter button is activated by pressing the enter key, shift is activated by pressing the s key, backspace is activated by pressing the backspace key, the exponentiation operation is activated by pressing the e key, and the four basic arithmetic operations (+, -, , /) are activated by pressing the corresponding keys.
为了方便输入,许多按钮被绑定到键盘快捷键上。按相应的数字键可以激活数字,按回车键可以激活回车键,按s键可以激活移位,按退格键可以激活退格,按e键可以激活指数化操作,按相应的键可以激活四个基本算术操作(+,-,,/)。
Finally, a few operations are semi-hidden. When not entering numbers, the backspace button drops the top entry from the stack, while the enter button duplicates the top entry on the stack. Some of these combinations are not intuitive and therefore might not represent very good GUI design. However, they do mimic the input used on the HP48S. If you have never used an HP48 series calculator before, I highly suggest building and familiarizing yourself with the GUI from the GitHub repository before continuing.
最后,有一些操作是半隐藏的。当不输入数字时,退格按钮会从Stack中删除最上面的条目,而输入按钮会复制Stack中最上面的条目。这些组合中的一些并不直观,因此可能不代表很好的GUI设计。然而,它们确实模仿了HP48S上使用的输入。如果你以前从未使用过HP48系列计算器,我强烈建议你在继续使用之前,先从GitHub仓库中建立并熟悉GUI。
If you’re wondering what a proc key does, it executes stored procedures. It is one of the “new” requirements we’ll encounter in Chapter 8.
如果你想知道proc键是做什么的,它可以执行存储过程。它是我们将在第8章遇到的 “新 “要求之一。
One’s first critique about the GUI might be that it is not very pretty. I agree. The purpose of the GUI in this chapter is not to demonstrate advanced Qt features. Rather, the purpose is to illustrate how to design a code base to be modular, robust, reliable, and extensible. Adding code to make the GUI more attractive rather than functional would distract from this message. Of course, the design permits a prettier GUI, so feel free to make your own pretty GUI on top of the provided infrastructure.
人们对GUI的第一个批评可能是它不太漂亮。我同意。本章中的GUI的目的不是为了展示高级的Qt功能。而是为了说明如何将代码库设计成模块化、健壮、可靠和可扩展的。添加代码来使GUI更有吸引力而不是功能性,会分散对这一信息的注意力。当然,这个设计允许更漂亮的GUI,所以你可以在所提供的基础架构上自由地制作你自己的漂亮GUI。
We now have sufficient detail to design and implement the calculator’s GUI. However, before we begin, a short discussion on alternatives for building GUIs is warranted.
我们现在有足够的细节来设计和实现计算器的图形用户界面。然而,在我们开始之前,有必要简短地讨论一下建立图形用户界面的替代方案。
6.2 Building GUIs
Essentially, two distinct paths exist for building a GUI: construct the GUI in an integrated development environment (IDE) or construct the GUI in code. Here, I loosely use the term code to indicate building the GUI by text, whether it be by using a traditional programming language like C++ or a declarative markup syntax like XML. Of course, between the two extremes is the hybrid approach, which utilizes elements from both IDEs and code.
从本质上讲,构建图形用户界面有两条不同的途径:在集成开发环境(IDE)中构建图形用户界面或在代码中构建图形用户界面。在这里,我宽泛地使用代码一词来表示通过文本构建GUI,无论是通过使用像C++这样的传统编程语言还是像XML这样的声明性标记语法。当然,在这两个极端之间的是混合方法,它利用了IDE和代码的元素。
6.2.1 Building GUIs in IDEs
If all you need is a simple GUI, then, certainly, designing and building your GUI in an IDE is the easier route. Most IDEs have a graphical interface for laying out visual elements onto a canvas, which, for example, might represent a dialog box or a widget. Once a new canvas is set up, the user visually builds the GUI by dragging and dropping existing widgets onto the canvas. Existing widgets consist of the built-in graphical elements of the GUI toolkit (e.g., a push button) as well as custom widgets that have been enabled for drag-and-drop in the IDE framework. Once the layout is complete, actions can be tied together either graphically or with a little bit of code. Ultimately, the IDE creates code corresponding to the graphically laid out GUI, and this IDE-created code is compiled with the rest of your source code.
如果你所需要的只是一个简单的图形用户界面,那么,在IDE中设计和建立你的图形用户界面当然是更容易的途径。大多数集成开发环境都有一个图形界面,用于在画布上布置视觉元素,例如,它可能代表一个对话框或一个部件。一旦建立了新的画布,用户就可以通过拖放现有的小部件到画布上来直观地建立GUI。现有的部件包括GUI工具包的内置图形元素(例如,一个按钮),以及在IDE框架中启用拖放的自定义部件。一旦布局完成,就可以用图形或少量的代码将动作捆绑起来。最终,IDE会创建与图形化布局的GUI相对应的代码,而这些IDE创建的代码会与你的其他源代码一起编译。
Building a GUI using an IDE has both advantages and disadvantages. Some of the advantages are as follows. First, because the process is visual, you can easily see the GUI’s appearance as you perform the layout. This is in direct contrast with writing code for the GUI, where you only see the look of the GUI after compiling and executing the code. The difference is very much akin to the difference between using a WYSIWYG text editor like Microsoft Word and a markup language like LaTeX for writing a paper. Second, the IDE works by automatically generating code behind the scenes, so the graphical approach can significantly reduce the amount of coding required to write a GUI. Third, IDEs typically list the properties of a GUI element in a property sheet, making it trivial to stylize a GUI without constantly consulting the API documentation. This is especially useful for rarely used features.
使用IDE构建GUI既有优势也有劣势。其中的一些优点如下。首先,由于这个过程是可视化的,你可以在执行布局时很容易看到GUI的外观。这与为GUI写代码形成了直接的对比,在那里你只能在编译和执行代码后看到GUI的外观。这种区别非常类似于使用所见即所得的文本编辑器(如Microsoft Word)和使用标记语言(如LaTeX)来写论文的区别。第二,IDE的工作原理是在幕后自动生成代码,所以图形化的方法可以大大减少编写GUI所需的编码量。第三,IDE通常在属性表中列出GUI元素的属性,使GUI的风格化变得微不足道,而无需不断查阅API文档。这对很少使用的功能特别有用。
Some of the disadvantages to using an IDE to build a GUI are as follows. First, you are limited to the subset of the API that the IDE chooses to expose. Sometimes the full API is exposed, and sometimes it is not. If you need functionality that the IDE’s author chose not to grant you, you’ll be forced into writing your own code. That is, the IDE may limit fine-tuned control of GUI elements. Second, for repetitive GUI elements, you may have to perform the same operation many times (e.g., clicking to make text red in all push buttons), while in code, it’s easy to encapsulate any repeated task in a class or function call. Third, using the IDE to design a GUI limits the GUI to decisions that can be made at compile time. If you need to dynamically change the structure of a GUI, you’ll need to write code for that. Fourth, designing a GUI in an IDE ties your code to a specific vendor product. In a corporate environment, this may not be a significant concern because the development environment may be uniform throughout the company. However, for an open source, distributed project, not every developer who might want to contribute to your codebase will want to be restricted to the same IDE you chose.
使用IDE来构建GUI的一些缺点如下。首先,你被限制在IDE选择公开的API的子集上。有时完整的API被公开,有时则没有。如果你需要IDE作者选择不授予你的功能,你将被迫写自己的代码。也就是说,IDE可能会限制对GUI元素的微调控制。第二,对于重复的GUI元素,你可能要多次执行相同的操作(例如,点击使所有的按钮中的文字变成红色),而在代码中,很容易将任何重复的任务封装在一个类或函数调用中。第三,使用集成开发环境来设计图形用户界面将图形用户界面限制在可以在编译时做出的决定。如果你需要动态地改变GUI的结构,你需要为此编写代码。第四,在集成开发环境中设计图形用户界面,将你的代码与特定的供应商产品联系在一起。在企业环境中,这可能不是一个重要的问题,因为整个公司的开发环境可能是统一的。然而,对于一个开源的、分布式的项目来说,并不是每个可能想为你的代码库做贡献的开发者都希望被限制在你选择的同一个IDE中。
6.2.2 Building GUIs in Code
Building a GUI in code is exactly what the name implies. Rather than graphically placing widgets on a canvas, you instead write code to interact with the GUI toolkit. Several different options exist for how the code can be written, and often, more than one option is available to you for any given GUI toolkit. First, you can almost always write source code in the language of the toolkit. For example, in Qt, you can build your GUI entirely by writing C++ in a very imperative style (i.e., you direct the GUI’s behavior explicitly). Second, some GUI toolkits permit a declarative style (i.e., you write markup code describing the style of GUI elements, but the toolkit defines the elements’ behaviors). Finally, some toolkits use a script-based interface for constructing a GUI (often JavaScript or a JavaScript derivative syntax) perhaps in conjunction with a declarative markup. In the context of this chapter, building a GUI in code refers exclusively to coding in C++ against Qt’s desktop widget set.
用代码构建GUI,正如其名称所暗示的那样。与其在画布上以图形方式放置部件,不如写代码与GUI工具包进行交互。对于如何编写代码,存在几种不同的选择,而且对于任何给定的GUI工具包来说,往往有一种以上的选择可供你使用。首先,你几乎总是可以用工具包的语言编写源代码。例如,在Qt中,你可以完全通过以非常命令式的方式编写C++来建立你的GUI(即,你明确地指导GUI的行为)。其次,一些GUI工具包允许使用声明式风格(即,你写标记代码描述GUI元素的风格,但工具包定义元素的行为)。最后,一些工具包使用基于脚本的界面来构建GUI(通常是JavaScript或JavaScript的衍生语法),也许与声明式标记相结合。在本章的上下文中,用代码构建GUI完全是指用C++对Qt的桌面小部件集进行编码。
As you might expect, building a GUI in code has nearly the opposite trade-offs as building a GUI with an IDE. The advantages are as follows. First, the full API to the widgets is completely exposed. Therefore, the programmer has as much fine- tuned control as desired. If the widget library designer wanted a user to be able to do something, you can do it in code. Second, repetitive GUI elements are easily managed through the use of abstraction. For example, in designing a calculator, instead of having to customize every button manually, we can create a button class and simply instantiate it. Third, adding widgets dynamically at runtime is easy. For pdCalc, this advantage will be important in fulfilling the requirement to support dynamic plugins. Fourth, designing a GUI in code grants complete IDE independence, provided that the build system is independent of the IDE.
正如你所期望的那样,在代码中构建图形用户界面与用集成开发环境构建图形用户界面有着几乎相反的权衡。其优点如下。首先,小工具的全部API是完全暴露的。因此,程序员可以根据需要进行微调控制。如果部件库设计者希望用户能够做一些事情,你可以在代码中做到这一点。第二,通过使用抽象,重复的GUI元素很容易被管理。例如,在设计一个计算器时,我们可以创建一个按钮类并简单地将其实例化,而不是手动定制每个按钮。第三,在运行时动态地添加部件很容易。对于pdCalc来说,这一优势对于满足支持动态插件的要求非常重要。第四,在代码中设计GUI可以完全独立于IDE,只要构建系统是独立于IDE的。
While building a GUI in code has many advantages, disadvantages exist as well. First, the layout is not visual. In order to see the GUI take shape, you must compile and execute the code. If it looks wrong, you have to tweak the code, try again, and repeat this process until you get it right. This can be exceedingly tedious and time consuming. Second, you must author all of the code yourself. Whereas an IDE will autogenerate a significant portion of the GUI code, particularly the parts related to the layout, when you are writing code, you must do all the work manually. Finally, when writing a GUI in code, you will not have access to all of a widget’s properties succinctly on a property sheet. Typically, you’ll need to consult the documentation more frequently. That said, good IDE code completion can help significantly with this task. Someone may cry foul to my last remark, claiming, “It’s unfair to indicate that using an IDE can mitigate a disadvantage of not using an IDE.” Remember, unless you’re writing your source code in a pure text editor (unlikely), the code editor is still likely a sophisticated IDE. My comparison is between building a GUI using an IDE’s graphical GUI layout tool versus writing the code manually using a modern code editor, likely itself an IDE.
虽然在代码中构建GUI有很多优点,但也存在缺点。首先,布局是不直观的。为了看到GUI的雏形,你必须编译和执行代码。如果它看起来不对,你必须调整代码,再次尝试,并重复这个过程,直到你得到它。这可能是极其乏味和耗时的。第二,你必须自己编写所有的代码。集成开发环境会自动生成很大一部分GUI代码,特别是与布局有关的部分,而当你编写代码时,你必须手动完成所有的工作。最后,当用代码编写GUI时,你将无法在属性表上简洁地访问一个小工具的所有属性。通常情况下,你需要更频繁地查阅文档。这就是说,好的IDE代码完成可以大大帮助完成这项任务。有人可能会对我的最后一句话喊冤,声称:”指出使用IDE可以减轻不使用IDE的缺点是不公平的。记住,除非你用纯文本编辑器写源代码(不太可能),否则代码编辑器仍然可能是一个复杂的IDE。我的比较是在使用IDE的图形化GUI布局工具构建GUI与使用现代代码编辑器(很可能本身就是IDE)手动编写代码之间。
6.2.3 Which GUI Building Method Is Better?
The answer to the overly general question in the section header is, of course, neither. Which technique is better for building a GUI is entirely context dependent. When you encounter this question in your own coding pursuits, consult the trade-offs above, and make the choice most sensible for your situation. Often, the best solution is a hybrid strategy where some parts of the GUI will be laid out graphically while other parts of the GUI will be built entirely from code.
对于本节标题中那个过于笼统的问题,答案当然是:都不是。哪种技术对构建GUI更好,完全取决于环境。当你在自己的编码追求中遇到这个问题时,请参考上面的权衡,并根据你的情况做出最明智的选择。通常情况下,最好的解决方案是一个混合策略,其中GUI的某些部分将以图形的方式布局,而GUI的其他部分将完全由代码构建。
A more specific question in our context is, “Which GUI building method is better for pdCalc?” For this application, the trade-offs heavily favor a code-based approach. First, the visual layout for the calculator is fairly trivial (a status window, a display widget, and a grid of buttons) and easily accomplished in code. This fact immediately removes the most significant advantage of the IDE approach: handling a complex layout visually. Second, the creation and layout of the buttons is repetitive but easily encapsulated, which is one of the advantages of a code-based approach. Finally, because the calculator must support runtime plugins, the code approach works better for dynamically adding widget elements (runtime discovered buttons).
在我们的背景下,一个更具体的问题是,”哪种GUI构建方法对pdCalc更好?” 对于这个应用来说,权衡利弊后,我们更倾向于采用基于代码的方法。首先,计算器的视觉布局是相当琐碎的(一个状态窗口、一个显示部件和一个按钮的网格),很容易在代码中完成。这一事实立即消除了IDE方法的最重要的优势:以视觉方式处理一个复杂的布局。其次,按钮的创建和布局是重复的,但很容易封装,这也是基于代码的方法的优点之一。最后,由于计算器必须支持运行时插件,代码方法在动态添加部件元素(运行时发现的按钮)时效果更好。
In the remainder of this chapter, we’ll explore the design of pdCalc’s GUI in code. In particular, the main emphasis will be on the design of components and their interfaces. Because our focus is not on widget construction, many implementation details will be glossed over. Never fear, however. If you are interested in the details, all of the code is available for your perusal in the GitHub repository.
在本章的其余部分,我们将探讨pdCalc的图形用户界面的代码设计。特别是,主要的重点是组件和它们的接口的设计。因为我们的重点不在部件的构造上,所以许多实现的细节将被忽略掉。不过,不要担心。如果你对这些细节感兴趣,所有的代码都可以在GitHub仓库中找到,供你阅读。
6.3 Modularization(模块化)
From the outset of this book, we have discussed decomposition strategies for the calculator. Using the MVC architectural pattern, we split our design into a model, a view, and a controller. In Chapter 4, we saw that one of the main components, the command dispatcher, was split into subcomponents. Whereas the CLI was simple enough to not need modularization, the GUI is sufficiently complex that decomposition is useful.
从本书的一开始,我们就讨论了计算器的分解策略。利用MVC架构模式,我们将设计分成了模型、视图和控制器。在第4章中,我们看到其中一个主要组件,即命令调度器,被分成了几个子组件。虽然CLI简单到不需要模块化,但GUI足够复杂,分解是有用的。
In Chapter 5, we determined that any user interface for our system must inherit from the UserInterface abstract class. Essentially, the UserInterface class defines the abstract interface of the view in the MVC pattern. While the GUI module must inherit from UserInterface and hence present the same abstract interface to the controller, we are free to decompose the internals of the GUI however we see fit. We’ll again use our guiding principles of loose coupling and strong cohesion to modularize the GUI.
在第五章中,我们确定我们系统的任何用户界面都必须继承自UserInterface抽象类。本质上,UserInterface类定义了MVC模式中视图的抽象接口。虽然GUI模块必须继承自UserInterface,从而向控制器提供相同的抽象接口,但我们可以自由地分解GUI的内部结构,只要我们认为合适。我们将再次使用松散耦合和高内聚的指导原则来实现GUI的模块化。
When I decompose a module, I first think in terms of strong cohesion. That is, I attempt to break the module into small components that each do one thing (and do it well). Let’s try that with the GUI. First, any Qt GUI must have a main window, defined by inheriting QMainWindow. The main window is also the entry point to the MVC view, so our main window must also inherit from UserInterface. The MainWindow is our first class. Next, visually inspecting Figure 6-1, the calculator is obviously divided into a component used for input (collection of buttons) and a component used for display. We therefore add two more classes, the InputWidget and the Display. We’ve already discussed that an advantage of using the code approach to building a GUI is to abstract the repeated creation of buttons, so we’ll make a CommandButton class as well. Finally, let’s add a component responsible for managing the look-and-feel of the calculator (e.g., fonts, margins, spacing, etc.), which is aptly named the LookAndFeel class. A component for stored procedure entry also exists, but we will delay the discussion of that component until Chapter 8.
当我分解一个模块时,我首先考虑的是高内聚。也就是说,我试图将模块分解成小的组件,每个组件只做一件事(而且要做得好)。让我们用GUI来试试。首先,任何Qt GUI都必须有一个主窗口,通过继承QMainWindow定义。主窗口也是MVC视图的入口,所以我们的主窗口也必须继承自UserInterface。MainWindow是我们的第一个类。接下来,目测图6-1,计算器显然被分成了一个用于输入的组件(按钮集合)和一个用于显示的组件。因此我们又添加了两个类,InputWidget和Display。我们已经讨论过,使用代码的方法来构建GUI的一个好处是可以抽象出按钮的重复创建,所以我们也要做一个CommandButton类。最后,让我们添加一个负责管理计算器的外观和感觉的组件(例如,字体、边距、间距等),它被恰当地命名为LookAndFeel类。一个用于存储过程输入的组件也存在,但我们将把这个组件的讨论推迟到第8章。
Let’s now look at the design of each class, starting with the CommandButton. We’ll discuss any necessary refinements to this initial decomposition if and when they arise.
现在让我们看看每个类的设计,从CommandButton开始。如果出现任何必要的细化,我们将讨论这个初始分解的问题。
6.3.1 The CommandButton Abstraction(命令按钮的抽象)
I begin the discussion by describing how buttons are abstracted. This is a sensible place to begin since buttons underlie the input mechanism for both numbers and commands to the calculator.
我在讨论中首先描述了按钮是如何被抽象化的。这是一个明智的开始,因为按钮是数字和命令的输入机制的基础。
Qt provides a push button widget class that displays a clickable button that emits a signal when the button is clicked. This QPushButton class provides the basis for the functionality that we require for number and command input. One prospective design we could employ would be to use QPushButtons as-is. This design would require explicitly writing code to connect each QPushButton manually to its own customized slot. However, this approach is repetitive, tedious, and highly error-prone. Moreover, some buttons need additional functionality not provided by the QPushButton API (e.g., shifted input). Therefore, we instead seek a button abstraction for our program that builds upon the QPushButton, supplements this Qt class with additional functionality, but also simultaneously restricts the QPushButton’s interface to meet exactly our requirements. We’ll call this class the CommandButton.
Qt提供了一个按钮部件类,显示一个可点击的按钮,当按钮被点击时发出信号。这个QPushButton类为我们需要的数字和命令输入的功能提供了基础。我们可以采用的一个前瞻性设计是按原样使用QPushButton。这种设计需要明确地编写代码,将每个QPushButton手动连接到它自己的定制槽。然而,这种方法是重复的、乏味的,而且非常容易出错。此外,有些按钮需要QPushButton API不提供的额外功能(例如,移位的输入)。因此,我们要为我们的程序寻找一个按钮抽象,它建立在QPushButton的基础上,用额外的功能来补充这个Qt类,但也同时限制了QPushButton的接口,以完全满足我们的要求。我们将这个类称为CommandButton。
In pattern parlance, we are proposing something that acts as both an adapter and a facade. We saw the adapter pattern in Chapter 3. The facade pattern is a close cousin. Whereas the adapter pattern is responsible for converting one interface into another (possibly with some adaptation), the facade pattern is responsible for providing a unified interface to a set of interfaces in a subsystem (often as a simplification). Our CommandButton is tasked with doing both. We are both simplifying the QPushButton interface to a restricted subset that pdCalc needs but simultaneously adapting QPushButton’s functionality to match the requirements of our problem. So, is CommandButton a facade or an adapter? The difference is semantic; it shares characteristics of each. Remember, it is important to understand the objectives of different patterns and adapt them according to your needs. Try not to get lost in rote implementations from the Gang of Four [6] for the sake of pattern purity.
用模式的说法,我们提出的东西既是一个适配器又是一个外观。我们在第三章看到了适配器模式。外观模式是一个近似的表亲。适配器模式负责将一个接口转换为另一个接口(可能要做一些调整),而外观模式则负责为子系统中的一组接口提供统一的接口(通常是简化)。我们的CommandButton的任务是做这两件事。我们既要将QPushButton接口简化为pdCalc需要的一个有限的子集,同时又要调整QPushButton的功能以符合我们问题的要求。那么,CommandButton是一个界面还是一个适配器?区别是语义上的;它具有各自的特征。记住,理解不同模式的目标并根据你的需要来调整它们是很重要的。尽量不要为了模式的纯粹性而迷失在四人帮[6]的生硬实现中。
6.3.2 The CommandButton Design(命令按钮的设计)
Introductory remarks aside, we still must determine what exactly our CommandButton needs to do and how it will interact with the rest of the GUI. In many ways, a CommandButton looks and acts similarly to a QPushButton. For example, a CommandButton must present a visual button that can be clicked, and after the button is clicked, it should emit some kind of signal to let other GUI components know a click action has occurred. Unlike a standard QPushButton, however, our CommandButton must support both a standard and shifted state (e.g., a button that supports both sin and arcsin). This support should be both visual (both states should be shown by our CommandButton widget) and functional (click signals must describe both a standard click and a shifted click).
除了介绍之外,我们仍然必须确定我们的CommandButton到底需要做什么,以及它将如何与GUI的其他部分交互。在许多方面,CommandButton的外观和行为都与QPushButton相似。例如,一个CommandButton必须呈现一个可以被点击的视觉按钮,在按钮被点击后,它应该发出某种信号,让其他GUI组件知道点击动作已经发生。然而,与标准的QPushButton不同,我们的CommandButton必须同时支持标准状态和移动状态(例如,一个同时支持sin和arcsin的按钮)。这种支持应该是视觉上的(两种状态都应该由我们的CommandButton部件显示)和功能上的(点击信号必须同时描述标准的点击和移位的点击)。
We therefore have two design questions to answer. First, how do we design and implement the widget to appear correctly on the screen? Second, how will the calculator, in general, handle shifted operations?
因此,我们有两个设计问题需要回答。首先,我们如何设计和实现小部件,使其正确地出现在屏幕上?第二,一般来说,计算器将如何处理移位的操作?
Let’s first address the CommandButton appearance problem. Sure, we could implement our button from scratch, paint the screen manually, and use mouse events to trap button clicks, but that’s overkill for CommandButton. Instead, we seek a solution that reuses Qt’s QPushButton class. We essentially have two options for reuse: inheritance and encapsulation.
让我们首先解决CommandButton的外观问题。当然,我们可以从头开始实现我们的按钮,手动绘制屏幕,并使用鼠标事件来捕获按钮的点击,但这对CommandButton来说是多余的。相反,我们寻求一种重用Qt的QPushButton类的解决方案。对于重用,我们基本上有两个选择:继承和封装。
First, let’s consider reusing the QPushButton class in the CommandButton class’s design via inheritance. This approach is reasonable since one could logically adopt the viewpoint that a CommandButton “is-a” QPushButton. This approach, however, suffers from an immediate deficiency. An “is-a” relationship implies public inheritance, which means that the entire public interface of QPushButton would become part of the public interface for CommandButton. However, we already determined that for simplicity within pdCalc, we want CommandButton to have a restricted interface (the facade pattern). OK, let’s try private inheritance and modify our viewpoint to an “implements-a” relationship between CommandButton and QPushButton. Now we encounter a second deficiency. Without public inheritance from QPushButton, CommandButton loses its indirect inheritance of the QWidget class, a prerequisite in Qt for a class to be a user interface object. Therefore, any implementation inheriting QPushButton privately would also require public inheritance from QWidget. However, because QPushButton also inherits from QWidget, the multiple inheritance of both of these classes by CommandButton would lead to ambiguities and is thus disallowed. We must seek an alternative design.
首先,让我们考虑在CommandButton类的设计中通过继承重用QPushButton类。这种方法是合理的,因为人们可以在逻辑上采用这样的观点:CommandButton“是一个”QPushButton。然而,这种方法有一个直接的缺陷。“is-a”关系意味着公共继承,这意味着QPushButton的整个公共接口将成为CommandButton的公共接口的一部分。然而,我们已经确定,为了在pdCalc中简化程序,我们希望CommandButton有一个受限的接口(外观模式)。好吧,让我们试试私有继承,并将我们的观点修改为CommandButton和QPushButton之间的“implements-a”关系。现在我们遇到了第二个缺陷。如果不公开继承QPushButton,CommandButton就失去了对QWidget类的间接继承,这是Qt中一个类成为用户界面对象的先决条件。因此,任何私下继承QPushButton的实现也需要从QWidget公开继承。然而,由于QPushButton也继承自QWidget,CommandButton对这两个类的多重继承会导致歧义,因此是不允许的。我们必须寻求另一种设计。
Now, consider encapsulating a QPushButton within a CommandButton (i.e., CommandButton “has-a” QPushButton). We probably should have started with this option since general practice indicates we should prefer encapsulation to inheritance whenever possible. However, many developers tend to start with inheritance, and I wanted to discuss the drawbacks of that approach without resorting merely to C++ canon. Aside from breaking the strong inheritance relationship, choosing an encapsulation approach overcomes the two drawbacks of using inheritance previously discussed. First, since the QPushButton will be encapsulated within a CommandButton, we are free to expose only those parts of the QPushButton interface (or none at all) that make sense for our application. Second, by using encapsulation, we’ll avoid the multiple inheritance mess of inheriting from both the QWidget and QPushButton classes simultaneously. Note that I do not object, in principle, to designs that use multiple inheritance. Multiple inheritance is simply ambiguous in this instance.
现在,考虑将一个QPushButton封装在一个CommandButton中(即CommandButton “有一个 “QPushButton)。我们也许应该从这个选项开始,因为一般的实践表明,只要有可能,我们应该首选封装而不是继承。然而,许多开发者倾向于从继承开始,我想讨论这种方法的缺点,而不是仅仅诉诸于C++教条。除了打破强继承关系外,选择封装方式还克服了之前讨论的使用继承的两个缺点。首先,由于QPushButton将被封装在CommandButton中,我们可以自由地只暴露QPushButton接口中那些对我们的应用有意义的部分(或者根本没有)。其次,通过使用封装,我们可以避免同时继承QWidget和QPushButton类的多重继承问题。请注意,原则上我并不反对使用多重继承的设计。在这种情况下,多重继承只是模棱两可。
Encapsulating relationships can either take the form of composition or aggregation. Which is right for the CommandButton class? Consider two classes, A and B, where A is encapsulating B. In a composite relationship, B is an integral part of A. In code, the relationship is expressed as follows:
封装关系可以采取组合或聚合的形式。对于CommandButton类来说,哪一种是正确的?考虑两个类,A和B,其中A是封装B的。在复合关系中,B是A的一个组成部分。在代码中,这种关系表示如下:
class A {
// ...
private:
B b_;
};
In contrast, aggregation implies that A is merely using a B object internally. In code, aggregation is expressed as follows:
相反,聚合意味着A只是在内部使用B对象。在代码中,聚合表示为:
class A {
// ...
private:
B* b_; // or some suitable smart pointer or reference
};
For our application, I think aggregation makes more sense. That is, our CommandButton uses a QPushButton rather than is composed from a QPushButton. The difference is subtle, and an equally logical argument could be made for declaring the relationship to be composition. That said, both designs work mechanically within Qt, so your compiler really won’t care how you choose to express the relationship.
对于我们的应用,我认为聚合更有意义。也就是说,我们的CommandButton使用了一个QPushButton,而不是由一个QPushButton组成。这种区别是微妙的,而且同样有逻辑的论点可以被用来声明这种关系是组合关系。也就是说,这两种设计在Qt中都能机械地工作,所以你的编译器真的不会关心你选择如何表达这种关系。
Now that we have decided to aggregate the QPushButton within the CommandButton, we can proceed with the overall design of the CommandButton class. Our CommandButton must support both a primary and secondary command. Visually, I chose to display the primary command on the button and the secondary command in blue above and to the left of the button. (I’ll discuss how the shifted state operates momentarily.) Therefore, the CommandButton merely instantiates a QPushButton and a QLabel and places them both in a QVBoxLayout. The QPushButton displays the text for the primary command, and the QLabel displays the text for the shifted command. The layout is depicted in Figure 6-2. To complete the design, as previously stated, in order to interact graphically with the rest of the GUI, the CommandButton must publicly inherit from the QWdiget class. The design results in a reusable CommandButton widget class for a generic push button, declaring both a primary and secondary command. Because the push button action is achieved by using a QPushButton, the overall implementation of the CommandButton class is remarkably simple.
现在我们已经决定在CommandButton中聚合QPushButton,我们可以继续进行CommandButton类的整体设计。我们的CommandButton必须同时支持一个主命令和一个辅助命令。在视觉上,我选择在按钮上显示主要命令,而在按钮的上方和左边用蓝色显示次要命令。(因此,CommandButton只是实例化了一个QPushButton和一个QLabel,并将它们放在一个QVBoxLayout中。QPushButton显示主命令的文本,而QLabel显示移位命令的文本。图6-2中描述了这个布局。为了完成这个设计,如前所述,为了与GUI的其他部分进行图形交互,CommandButton必须公开继承自QWdiget类。这个设计的结果是一个可重复使用的CommandButton部件类,用于一个通用的按钮,同时声明一个主命令和次命令。因为按下按钮的动作是通过使用QPushButton实现的,所以CommandButton类的整体实现非常简单。
Figure 6-2. The layout of the CommandButton
One final, small detail for reusing the QPushButton remains. Obviously, because the QPushButton is encapsulated privately in the CommandButton, clients cannot externally connect to the QPushButton’s clicked() signal, rendering it impossible for client code to know when a CommandButton is clicked. This design is actually intentional. The CommandButton will internally trap the QPushButton’s clicked() signal and subsequently re-emit its own signal. The design of this public CommandButton signal is intricately linked to the handling of the shifted state.
重用QPushButton的最后一个小细节仍然存在。很明显,由于QPushButton被封装在CommandButton中,客户端不能从外部连接到QPushButton的clicked()信号,使得客户端代码无法知道CommandButton何时被点击。这种设计实际上是故意的。CommandButton将在内部捕获QPushButton的clicked()信号,并随后重新发出自己的信号。这个公共CommandButton信号的设计与移位状态的处理有着复杂的联系。
We now return to modeling the shifted state within the calculator. We have two practical options. The first option is to have CommandButton understand when the calculator is in the shifted state and only signal the correct shifted or unshifted command. Alternatively, the second option is to have CommandButton signal with both the shifted and unshifted commands and let the receiver of the signal sort out the calculator’s current state. Let’s examine both options.
我们现在回到对计算器内的移位状态进行建模。我们有两个实际的选择。第一个选择是让CommandButton理解计算器何时处于移位状态,并且只发出正确的移位或不移位命令的信号。或者,第二种选择是让CommandButton同时发出移位和非移位命令的信号,让信号的接收者整理出计算器的当前状态。让我们来研究一下这两个选项。
The first option, having CommandButton know if the calculator is in a shifted or unshifted state, is fairly easy to implement. In one implementation, the shift button notifies every button (via Qt signals and slots) when it is pressed, and the buttons toggle between the shifted and unshifted state. If desired, one could even swap the text in the shift position with the text on the button every time the shifted state is toggled. Alternatively, the shift button can be connected to one slot that sets a global shift state flag that buttons can query when they signal that a click has occurred. In either implementation scenario, when the button is clicked, only the command for the current state is signaled, and the receiver of this command eventually forwards the single command out of the GUI via a commandEntered() event.
第一个选项,让CommandButton知道计算器是处于移位还是未移位的状态,是相当容易实现的。在一个实现中,当移位按钮被按下时,它通知了每个按钮(通过Qt信号和槽),并且按钮在移位和非移位状态之间切换。如果需要,我们甚至可以在每次切换移位状态时,将移位位置的文本与按钮上的文本进行交换。另外,移位按钮可以连接到一个槽,这个槽设置了一个全局移位状态标志,按钮在发出点击信号时可以查询。在这两种实现方案中,当按钮被点击时,只有当前状态的命令被发出信号,这个命令的接收者最终通过commandEntered()事件将单个命令转发到GUI之外。
In the second option, the CommandButton is not required to know anything about the calculator’s state. Instead, when a button is clicked, it signals the click with both the shifted and unshifted states. Essentially, a button just informs its listeners when it is clicked and provides both possible commands. The receiver is then responsible for determining which of the possible commands to raise in the commandEntered() event. The receiver presumably must be responsible for tracking the shifted state (or be able to poll another class or variable holding that state).
在第二个选项中,CommandButton不需要知道任何关于计算器状态的信息。相反,当一个按钮被点击时,它用移位和未移位的状态发出点击信号。本质上,一个按钮只是在被点击时通知它的听众,并提供两种可能的命令。然后,接收器负责确定在commandEntered()事件中提出哪个可能的命令。接收者大概必须负责跟踪移位的状态(或者能够轮询持有该状态的另一个类或变量)。
For the CommandButton, both designs for handling the calculator’s state work fairly well. However, personally, I prefer the design that does not require CommandButton to know anything about the shifted state. In my opinion, this design promotes better cohesion and looser coupling. The design is more cohesive because a CommandButton should be responsible for displaying a clickable widget and notifying the system when the button is clicked. Requiring CommandButton to understand calculator states encroaches on the independence of their abstraction. Instead of just being generic clickable buttons with two commands, the buttons become integrally tied to the concept of the calculator’s global state. Additionally, by forcing CommandButton to understand the calculator’s state, the coupling in the system is increased by forcing CommandButton to be unnecessarily interconnected to either the shift button or to the class they must poll. The only advantage gained by notifying every CommandButton when the shift button is pressed is the ability to swap the labels for the primary and secondary commands. Of course, label swapping could be implemented independently of the CommandButton’s signal arguments.
对于CommandButton,两种处理计算器状态的设计都相当好。然而,我个人更喜欢不要求CommandButton知道任何关于移动状态的设计。在我看来,这种设计促进了更好的内聚力和更松散的耦合。这种设计更有凝聚力,因为CommandButton应该负责显示一个可点击的部件并在按钮被点击时通知系统。要求CommandButton理解计算器状态侵犯了其抽象的独立性。这些按钮不再是具有两个命令的通用可点击按钮,而是与计算器的全局状态的概念紧密相连。此外,通过强迫CommandButton理解计算器的状态,系统中的耦合度增加了,它迫使CommandButton不必要地与shift按钮或它们必须轮询的类相互联系。当按下shift按钮时,通知每个CommandButton的唯一好处是能够交换主要和次要命令的标签。当然,标签的交换可以独立于CommandButton的信号参数来实现。
6.3.3 The CommandButton Interface
Getting the design right is the hard part. With the design in hand, the interface practically writes itself. Let’s examine a simplified version of the CommandButton class’s definition, shown in Listing 6-1.
把设计做好是最难的部分。有了设计,界面实际上是自己写的。让我们来看看清单6-1中所示的CommandButton类定义的简化版本。
Listing 6-1. The CommandButton Class
class CommandButton : public QWidget {
Q_OBJECT // needed by all Qt objects with signals and slots
public : CommandButton(const string& dispPrimaryCmd, const string& primaryCmd,
const string& dispShftCmd, const string& shftCmd,
QWidget* parent = nullptr);
CommandButton(const string& dispPrimaryCmd, const string& primaryCmd,
QWidget* parent = nullptr);
private slots:
void onClicked();
signals:
void clicked(string primCmd, string shftCmd);
};
The CommandButton class has two constructors: the four-argument overload and the two-argument overload. The four-argument overload permits specification of both a primary command and a secondary command, while the two-argument overload permits the specification of only a primary command. Each command requires two strings for full specification. The first string equates to the text the label will present in the GUI, either on the button or in the shifted command location. The second string equates to the text command to be raised by the commandEntered() event. One could simplify the interface by requiring these two strings to be identical. However, I chose to add the flexibility of displaying a different text than that required by the command dispatcher. Note that we require overloads instead of default arguments due to the trailing parent pointer.
CommandButton类有两个构造函数:四个参数重载和两个参数重载。四参数重载允许指定一个主命令和一个辅助命令,而双参数重载只允许指定一个主命令。每条命令都需要两个字符串进行完整说明。第一个字符串相当于标签在GUI中显示的文本,可以是在按钮上,也可以是在移位的命令位置。第二个字符串相当于由commandEntered()事件引发的文本命令。我们可以通过要求这两个字符串相同来简化界面。然而,我选择增加灵活性,显示与命令调度器要求不同的文本。注意我们需要重载而不是默认参数,因为有尾部的父指针。
The only other public part of the interface is the clicked() signal that is emitted with both the primary and shifted commands for the button. The rationale behind a twoargument versus one-argument signal was previously discussed. Despite being private, I also listed the onClicked() slot in CommandButton’s interface to highlight the private slot that must be created to catch the internal QPushButton’s clicked() signal. The onClicked() function’s sole purpose is to trap the QPushButton’s clicked() signal and instead emit the CommandButton’s clicked() signal with the two function arguments.
接口中唯一的其他公共部分是clicked()信号,该信号在按钮的主命令和移位命令中都会发出。前面已经讨论过双参数信号与单参数信号的原理。尽管是私有的,我还是在CommandButton的接口中列出了onClicked()槽,以强调必须创建私有槽来捕捉内部QPushButton的clicked()信号。onClicked()函数的唯一目的是捕获QPushButton的clicked()信号,而不是用两个函数参数发射CommandButton的clicked()信号。
If you look at the actual declaration of the CommandButton class in CommandButton.h, you will see a few additional functions as part of CommandButton’s public interface. These are simply forwarding functions that either change the appearance (e.g., text color) or add visual elements (e.g., a tool tip) to the underlying QPushButton. While these functions are part of CommandButton’s interface, they are functionally optional and are independent of CommandButton’s underlying design.
如果你看一下CommandButton.h中CommandButton类的实际声明,你会看到一些额外的函数作为CommandButton的公共接口的一部分。这些都是简单的转发函数,它们要么改变外观(如文本颜色),要么向底层的QPushButton添加视觉元素(如工具提示)。虽然这些函数是CommandButton接口的一部分,但它们在功能上是可选的,与CommandButton的底层设计无关。
6.3.4 Getting Input
The GUI is required to take two distinct types of inputs from the user: numbers and commands. Both input types are entered by the user via CommandButtons (or keyboard shortcuts mapped to these buttons) arranged in a grid. This collection of CommandButtons, their layout, and their associated signals to the rest of the GUI compose the InputWidget class.
GUI需要从用户那里接受两种不同类型的输入:数字和命令。这两种输入类型都是由用户通过排列在一个网格中的CommandButtons(或映射到这些按钮的键盘快捷键)输入的。这个CommandButtons的集合,它们的布局,以及它们与GUI其他部分的相关信号构成了InputWidget类。
Command entry is conceptually straightforward. A CommandButton is clicked, and a signal is emitted, reflecting the command for that particular button. Ultimately, another part of the GUI will receive this signal and raise a commandEntered() event to be handled by the command dispatcher.
命令输入在概念上是直截了当的。一个CommandButton被点击,一个信号被发射出来,反映了该特定按钮的命令。最终,GUI的另一部分将收到这个信号,并引发一个commandEntered()事件,由命令调度器来处理。
Entering numbers is a bit more complicated than entering commands. In the CLI, we had the luxury of simply allowing the user to type numbers and press enter when the input was complete. In the GUI, however, we have no such built-in mechanism (assuming we want a GUI more sophisticated than a CLI in a Qt window). While the calculator does have a Command for entering numbers, remember that it assumes complete numbers, not individual digits. Therefore, the GUI must have a mechanism for constructing numbers.
输入数字比输入命令要复杂一些。在CLI中,我们可以简单地让用户输入数字,并在输入完成后按回车键。然而,在GUI中,我们没有这样的内置机制(假设我们想要一个比Qt窗口中的CLI更复杂的GUI)。虽然计算器确实有一个输入数字的命令,但请记住,它假设的是完整的数字,而不是单个数字。因此,GUI必须有一个构建数字的机制。
Building a number consists of entering digits as well as special symbols such as the decimal point, the plus/minus operator, or the exponentiation operator. Additionally, as the user types, he might make errors, so we’ll want to enable basic editing (e.g., backspace), as well. The assembly of numbers is a two-step process. The InputWidget is only responsible for emitting the button clicks required for composing and editing numbers. Another part of the GUI will receive these signals and assemble complete number input.
建立一个数字包括输入数字以及特殊符号,如小数点、加减运算符或指数运算符。此外,当用户输入时,他可能会出错,所以我们也要启用基本的编辑功能(例如,退格)。数字的组装是一个两步的过程。InputWidget只负责发出组成和编辑数字所需的按钮点击。GUI的另一部分将接收这些信号并组装完整的数字输入。
6.3.5 The Design of the InputWidget
Conceptually, the design of the InputWidget class is straightforward. The widget must display the buttons needed for generating and editing input, bind these buttons to keys (if desired), and signal when these buttons are clicked. As previously mentioned, the InputWidget contains buttons for both digit entry and command entry. Therefore, it is responsible for the digits 0-9, the plus/minus button, the decimal button, the exponentiation button, the enter button, the backspace button, the shift button, and a button for each command. Recall that as an economization, the CommandButton class permits two distinct commands per visual button.
从概念上讲,InputWidget类的设计是简单明了的。这个部件必须显示生成和编辑输入所需的按钮,将这些按钮与键绑定(如果需要),并在这些按钮被点击时发出信号。如前所述,InputWidget包含数字输入和命令输入的按钮。因此,它负责数字0-9、加/减按钮、小数点按钮、指数化按钮、回车按钮、退格按钮、移位按钮,以及每个命令的按钮。回顾一下,作为一种节约,CommandButton类允许每个可视化按钮有两个不同的命令。
For consistency throughout the GUI, we’ll use the CommandButton exclusively as the representation for all of the input buttons, even for buttons that neither issue commands nor have secondary operations (e.g., the 0 button). How convenient that our design for the CommandButton is so flexible! However, that decision still leaves us with two outstanding design issues. How do we lay out the buttons visually, and what do we do when a button is clicked?
为了整个GUI的一致性,我们将专门使用CommandButton来表示所有的输入按钮,即使是那些既不发布命令也没有二级操作的按钮(例如0按钮)。我们对CommandButton的设计是如此的灵活,这是多么方便啊 然而,这个决定仍然给我们留下了两个悬而未决的设计问题。我们如何在视觉上布置这些按钮,以及当一个按钮被点击时,我们该怎么做?
Two options exist for placing buttons in the InputWidget. First, the InputWidget itself owns a layout, it places all the buttons in this internal layout, and then the InputWidget itself can be placed somewhere on the main window. The alternative is for the InputWidget to accept an externally owned layout during construction and place its CommandButtons on that layout. In general, having the InputWidget own its own layout is the superior design. It has improved cohesion and decreased coupling over the alternative approach. The only exception where having the InputWidget accept an external layout would be preferred would be if the design called for other classes to share the same layout for the placement of additional widgets. In that special case, using a shared layout owned externally to both classes would be cleaner.
在InputWidget中放置按钮有两个选择。首先,InputWidget本身拥有一个布局,它把所有的按钮放在这个内部布局中,然后InputWidget本身可以被放在主窗口的某个地方。另一个选择是InputWidget在构建过程中接受一个外部拥有的布局,并将它的CommandButtons放在该布局上。一般来说,让InputWidget拥有它自己的布局是一个优越的设计。与其他方法相比,它提高了内聚力并降低了耦合度。让InputWidget接受外部布局的唯一例外是,如果设计要求其他类共享相同的布局来放置额外的小部件。在这种特殊情况下,使用两个类外部拥有的共享布局会更干净。
Let’s now turn our attention to what happens when a button is clicked within the InputWidget. Because the InputWidget encapsulates the CommandButtons, the clicked() signal for each CommandButton is not directly accessible to consumers of the InputWidget class. Therefore, the InputWidget must catch all of its CommandButtons’ clicks and re-emit them. For calculator commands like sine or tangent, re-emitting the click is a trivial forwarding command. In fact, Qt enables a shorthand notation for connecting a CommandButton’s clicked() signal directly to an InputWidget commandEntered() signal, forgoing the need to pass through a private slot in the InputWidget. Digits, number editing buttons (e.g., plus/minus, backspace), and calculator state buttons (e.g., shift) are better handled by catching the particular clicked() signal from the CommandButton in a private slot in the InputWidget and subsequently emitting a InputWidget signal for each of these actions.
现在让我们把注意力转移到InputWidget中的按钮被点击时的情况。因为InputWidget封装了CommandButtons,每个CommandButton的clicked()信号不能被InputWidget类的消费者直接访问。因此,InputWidget必须捕捉所有CommandButtons的点击并重新发送。对于像正弦或正切这样的计算器命令,重新发射点击是一个微不足道的转发命令。事实上,Qt提供了一个速记符号,用于将CommandButton的clicked()信号直接连接到InputWidget的commandEntered()信号,而不需要通过InputWidget的一个私有槽。数字、数字编辑按钮(如加/减、退格)和计算器状态按钮(如移位)最好通过在InputWidget的一个私有槽中捕捉CommandButton的特定clicked()信号来处理,随后为每个动作发射一个InputWidget信号。
As just described, as each input button is pressed, the InputWidget must emit its own signal. At one extreme, the InputWidget could have individual signals for each internal CommandButton. At the other extreme, the InputWidget could emit only one signal regardless of the button pressed and differentiate the action via an argument. As expected, for our design, we’ll seek some middle ground that shares elements from each extreme.
正如刚才所描述的,当每个输入按钮被按下时,InputWidget必须发出自己的信号。在一个极端,InputWidget可以为每个内部的CommandButton设置单独的信号。在另一个极端,InputWidget可以只发出一个信号,而不管按的是什么按钮,并通过一个参数来区分动作。正如预期的那样,在我们的设计中,我们将寻求一些中间地带,分享来自每个极端的元素。
Essentially, the InputWidget accepts three distinct types of input: a modifier (e.g., enter, backspace, plus/minus, shift), a scientific notation character (e.g., 0-9, decimal, exponentiation), or a command (e.g., sine, cosine, etc.). Each modifier requires a unique response; therefore, each modifier binds to its own separate signal. Scientific notation characters, on the other hand, can be handled uniformly simply by displaying the input character on the screen (the role of the Display class). Thus, scientific notation characters are all handled by emitting a single signal that encodes the specific character as an argument. Finally, commands are handled by emitting a single signal that simply forwards the primary and secondary commands verbatim as function arguments to the signal.
基本上,InputWidget接受三种不同类型的输入:一个修改器(例如,回车,退格,加/减,移位),一个科学符号(例如,0-9,十进制,指数),或一个命令(例如,正弦,余弦,等等)。每个修饰符都需要一个独特的响应;因此,每个修饰符都与它自己的独立信号绑定。另一方面,科学符号字符可以通过在屏幕上显示输入字符来统一处理(显示类的作用)。因此,科学符号的字符都是通过发出一个信号来处理的,该信号将特定的字符编码为一个参数。最后,命令是通过发出一个信号来处理的,该信号只是将主要和次要的命令作为函数参数逐字转发给信号。
In constructing the signal handling, it is important to maintain the InputWidget as a class for signaling raw user input to the rest of the GUI. Having the InputWidget interpret button presses leads to problems. For example, suppose we designed the InputWidget to aggregate characters and only emit complete, valid numbers. Since this strategy implies that no signal would be emitted per character entry, characters could neither be displayed nor edited until the number was completed. This situation is obviously unacceptable, as a user would definitely expect to see each character on the screen as she entered it.
在构建信号处理时,重要的是保持InputWidget作为一个向GUI的其他部分发出原始用户输入信号的类。让InputWidget解释按钮的按下导致了一些问题。例如,假设我们把InputWidget设计成聚合字符,只发出完整、有效的数字。由于这种策略意味着每个字符的输入都不会发出信号,所以在数字完成之前,既不能显示也不能编辑字符。这种情况显然是不可接受的,因为用户肯定希望在她输入每个字符时能在屏幕上看到它。
Let’s now turn our attention to translating our design into a minimal interface for the InputWidget.
现在让我们把注意力转向将我们的设计转化为InputWidget的最小界面。
6.3.6 The Interface of the InputWidget
Let’s begin the discussion of the InputWidget’s interface by presenting the class declaration. As expected, our clear design leads to a straightforward interface. See Listing 6-2.
让我们通过介绍类的声明来开始讨论InputWidget的接口。正如预期的那样,我们清晰的设计导致了一个简单的界面。参见清单6-2。
Listing 6-2. The InputWidget
class InputWidget : public QWidget {
Q_OBJECT
public:
explicit InputWidget(QWidget* parent = nullptr);
signals:
void characterEntered(char c);
void enterPressed();
void backspacePressed();
void plusMinusPressed();
void shiftPressed();
void commandEntered(string, string);
};
Essentially, the entire class interface is defined by the signals corresponding to user input events. Specifically, we have one signal indicating entry of any scientific notation character, one signal to forward command button clicks, and individual signals indicating clicking of the backspace, enter, plus/minus, or shift buttons, respectively.
基本上,整个类的界面是由对应于用户输入事件的信号来定义的。具体来说,我们有一个信号表示输入任何科学符号的字符,一个信号表示前进命令按钮的点击,以及分别表示点击退格、回车、加减或移位按钮的个别信号。
If you look in the GitHub repository source code in the InputWidget.cpp file, you will find a few additional public functions and signals. These extra functions are necessary to implement two features introduced in subsequent chapters. First, an addCommandButton() function and a setupFinalButtons() function are needed to accommodate the dynamic addition of plugin buttons, a feature introduced in Chapter 7. Second, a procedurePressed() signal is needed to indicate a user request to use a stored procedure. Stored procedures are introduced in Chapter 8.
如果你在GitHub仓库的源代码中查看InputWidget.cpp文件,你会发现一些额外的公共函数和信号。这些额外的函数对于实现后续章节中介绍的两个功能是必要的。首先,一个addCommandButton()函数和一个setupFinalButtons()函数是必要的,以适应动态添加插件按钮,这是第七章中介绍的功能。其次,需要一个procedurePressed()信号来表示用户请求使用一个存储过程。存储过程在第8章中介绍。
6.4 The Display
Conceptually, the calculator has two displays, one for input and one for output. This abstraction can be implemented visually either as two separate displays or as one merged input/output display. Both designs are perfectly valid; each is illustrated in Figure 6-3.
从概念上讲,计算器有两个显示屏,一个用于输入,一个用于输出。这一抽象概念既可以在视觉上实现为两个独立的显示,也可以实现为一个合并的输入/输出显示。这两种设计都是完全有效的;图6-3中分别说明了这一点。
Figure 6-3. Input and output display options
Choosing one style of I/O versus the other ultimately reduces to the customer’s preference. Having no particular affinity for either style, I chose a merged display because it looks more like the display of my HP48S calculator. With a display style chosen, let’s now focus on the design implications this choice implies.
选择一种I/O风格还是另一种,最终取决于客户的偏好。我对这两种风格都没有特别的好感,我选择了一个合并的显示屏,因为它看起来更像我的HP48S计算器的显示屏。在选择了显示风格之后,现在让我们把注意力集中在这个选择所意味着的设计含义上。
With a separate on-screen widget for input and output, as seen in Figure 6-3a, the choice to have separate input and output display classes would be obvious. The input display would have slots to receive the InputWidget’s signals, and the output display would have slots to receive completed numbers (from the input display) and stack updates. The cohesion would be strong, and the separation of components would be appropriate.
如图6-3a所示,在屏幕上有一个单独的输入和输出小部件,选择单独的输入和输出显示类是很明显的。输入显示将有插槽来接收InputWidget的信号,而输出显示将有插槽来接收完成的数字(来自输入显示)和堆栈更新。内聚力会很强,组件的分离也会很恰当。
Our design, however, calls for a commingled input/output display, as seen in Figure 6-3b. The commingled design significantly alters the sensibility of using independent input and output display classes. While lumping input and output display concerns into one class does decrease the cohesion of the display, trying to maintain two independent classes both pointing to the same on-screen widget would lead to an awkward implementation. For example, choosing which class should own the underlying Qt widget is arbitrary, likely resulting in a shared widget design (using a shared_ptr, perhaps?). However, in this scenario, should the input or the output display class initialize the on-screen widget? Would it make sense for the input display to signal the output display if the input display shared a pointer to the single display widget? The answer is simply that a two-class design is not tenable for a merged I/O display widget even though we might prefer to separate input and output display concerns.
然而,我们的设计需要一个混合的输入/输出显示,如图6-3b所示。混合的设计大大改变了使用独立的输入和输出显示类的感觉。虽然把输入和输出显示的关注点放在一个类中确实降低了显示的内聚力,但试图维持两个独立的类都指向同一个屏幕上的小部件会导致一个尴尬的实现。例如,选择哪个类应该拥有底层的Qt widget是任意的,可能会导致共享widget的设计(也许使用shared_ptr?然而,在这种情况下,应该由输入还是输出显示类来初始化屏幕上的widget?如果输入显示共享一个指向单一显示部件的指针,那么输入显示向输出显示发出信号是否有意义?答案很简单,对于一个合并的I/O显示部件来说,两类设计是站不住脚的,即使我们可能更喜欢把输入和输出显示的问题分开。
The aforementioned discussion identifies a few interesting points. First, the visual presentation of the design on screen can legitimately alter the design and implementation of the underlying components. While this may seem obvious once presented with a concrete GUI example, the indirect implication is that GUI class design may need to change significantly if the on-screen widgets are changed only slightly. Second, situations exist where the result is cleaner when the design directly contradicts the elements of good design postulated in Chapter 2. Obviously, the guidelines in Chapter 2 are meant to aid the design process, not to serve as inviolable rules. That said, my general advice is to aim to preserve clarity over adherence to guidelines, but only violate best practices judiciously.
前面提到的讨论指出了几个有趣的问题。首先,设计在屏幕上的视觉表现可以合理地改变底层组件的设计和实现。虽然一旦用一个具体的GUI例子来介绍,这可能是显而易见的,但间接的含义是,如果屏幕上的小部件只是稍作改变,GUI类的设计可能需要大大改变。第二,存在这样的情况:当设计直接与第二章中假设的良好设计的元素相矛盾时,其结果是更干净的。很明显,第二章中的指导方针是为了帮助设计过程,而不是作为不可侵犯的规则。也就是说,我的一般建议是,在遵守准则的基础上保持清晰度,但只需审慎地违反最佳实践。
Now that we’ve decided to pursue a single I/O display with a single underlying Display class, let’s look at its design.
现在我们已经决定用一个底层的Display类来追求单一的I/O显示,让我们看看它的设计。
6.4.1 The Design of the Display Class
I confess. My original design and implementation for the Display class was inept. Instead of using proper analysis techniques and upfront design, I grew the design organically (that is, alongside the implementation). However, as soon as my design forced the Display class to emit commandEntered() signals for the GUI to function properly, I knew the design had a “bad smell” to it. The class responsible for painting numbers on the screen should probably not be interpreting commands. That said, the implementation worked properly, so I left the code as it was and completed the calculator. However, when I finally started writing about the design, I had so much difficulty trying to formulate a rationale for my design that I finally had to admit to myself that the design was fatally flawed and desperately needed a rewrite.
我承认。我最初对Display类的设计和实现是不合格的。我没有使用适当的分析技术和前期设计,而是有机地增长了设计(也就是说,与实现同时进行)。然而,当我的设计迫使Display类发出commandEntered()信号以使GUI正常工作时,我就知道这个设计有一种 “坏味道”。负责在屏幕上画数字的类也许不应该解释命令。尽管如此,这个实现还是正常的,所以我把代码保持原样,完成了计算器。然而,当我终于开始写这个设计的时候,我在试图为我的设计提出一个理由时遇到了很大的困难,最后我不得不承认这个设计有致命的缺陷,迫切需要重写。
Obviously, after redesigning the display, I could have simply chosen to describe only the improved product. However, I think it is instructive to study my first misguided attempt, to discuss the telltale signs that the design had some serious problems, and finally to see the design that eventually emerged after a night of refactoring. Possibly, the most interesting lesson here is that bad designs can certainly lead to working code, so never assume that working code is an indicator of a good design. Additionally, bad designs, if localized, can be refactored, and sometimes refactoring should be undertaken solely to increase clarity. Refactoring, of course, assumes your project schedule contains enough contingency time to pause periodically just to pay down technical debt. Let’s briefly study my mistake before returning to a better design.
显然,在重新设计了显示器之后,我本可以简单地选择只描述改进后的产品。然而,我认为研究一下我第一次错误的尝试,讨论一下这个设计有一些严重问题的蛛丝马迹,最后看看经过一夜的重构后最终出现的设计,是很有启发的。可能,这里最有趣的教训是,糟糕的设计当然可以导致有效的代码,所以永远不要认为有效的代码是一个好设计的指标。此外,糟糕的设计,如果是局部的,也可以被重构,而且有时重构应该仅仅是为了提高清晰度。当然,重构的前提是你的项目计划包含足够的应急时间,可以定期暂停,只是为了偿还技术债务。在回到更好的设计之前,让我们简单地研究一下我的错误。
6.4.2 A Poor Design
From the analysis above, we determined that the calculator should have one unified Display class for handling both input and output. The fundamental mistake in my design for the display derived from incorrectly interpreting that one Display class implied no additional classes for orthogonal concerns. Hence, I proceeded to lump all functionality not handled by the InputWidget class into a single Display class. Let’s start along that path. However, rather than completing the design and implementation as I had previously done, we’ll stop and redesign the class as soon as we see the first fatal flaw emerge (which is what I should have done originally).
从上面的分析中,我们确定计算器应该有一个统一的显示类来处理输入和输出。我在设计显示器时的根本错误在于,我错误地认为一个显示器类意味着没有额外的类来处理正交的问题。因此,我继续把所有没有被InputWidget类处理的功能都归入一个Display类。让我们沿着这条道路开始吧。然而,我们不是像以前那样完成设计和实现,而是在看到第一个致命的缺陷出现时就停下来,重新设计这个类(这是我最初应该做的)。
With a single Display class design, the Display is responsible for showing input from the user and output from the calculation engine. Showing the output is trivial. The Display class observes the stackChanged() event (indirectly, since it is not part of the GUI’s external interface), and updates the screen display widget (a QLabel, in this case) with the new stack values. Conceptually, showing the input is trivial as well. The Display directly receives the signals emitted by the InputWidget class (e.g., characterEntered()) and updates the screen display widget with the current input. The simplicity of this interaction belies the fundamental problem with this design, which is that the input is not entered atomically for display. Instead, it is assembled over multiple signals by entering several characters independently and finalizing the input by pressing the enter button. This sequential construction of the input implies that the calculator must maintain an active input state, and input state has no business existing in a display widget.
在一个单一的Display类设计中,Display负责显示用户的输入和计算引擎的输出。显示输出是微不足道的。显示类观察stackChanged()事件(间接地,因为它不是GUI外部接口的一部分),并且用新的堆栈值更新屏幕显示部件(在这里是一个QLabel)。从概念上讲,显示输入也是很简单的。显示器直接接收由InputWidget类发出的信号(例如,characterEntered()),并用当前的输入来更新屏幕显示部件。这种交互的简单性掩盖了这种设计的基本问题,即输入不是以原子方式输入显示的。相反,它是通过独立输入几个字符,并通过按回车键最终完成输入,从而在多个信号中组合起来的。这种输入的顺序构造意味着计算器必须保持一个活跃的输入状态,而输入状态在显示小部件中是不存在的。
So what, aside from ideological aversion, is wrong with the Display class maintaining an input state? Can’t we just view the state as simply a display input buffer? Let’s follow through with this design to see why it is flawed. Consider, for example, the backspace button, whose operation is overloaded based on the input state. If the current input buffer is nonempty, the backspace button erases one character from this buffer. However, if the current input buffer is empty, pressing the backspace button causes the issuance of the command to drop the top number from the stack. Since, under this design, the Display owns the input state and is the sink for the backspacePressed() signal, the Display must be the source of the dropped number from the stack command. Once the Display starts issuing commands, we’ve completely given up on cohesion, and it’s time to find the pasta sauce because spaghetti code ensues. From here, instead of just abandoning the design, I doubled down, and my original design actually got worse. However, instead of proceeding further along this misguided path, let’s simply move on to examining a better approach.
那么,除了意识形态上的厌恶之外,Display类维持一个输入状态有什么问题呢?我们就不能把这个状态看作是一个简单的显示输入缓冲区吗?让我们通过这个设计来看看为什么它有缺陷。例如,考虑一下退格按钮,它的操作是基于输入状态的重载。如果当前的输入缓冲区不是空的,退格按钮就会从这个缓冲区中删除一个字符。然而,如果当前的输入缓冲区是空的,按退格按钮会导致发出的命令从堆栈中删除最上面的数字。因为在这个设计中,Display拥有输入状态,并且是backspacePressed()信号的汇入点,所以Display必须是从Stack中删除数字的命令的来源。一旦Display开始发布命令,我们就完全放弃了内聚力,是时候找到意大利面酱了,因为意大利面条代码随之而来。从这里开始,我没有直接放弃设计,而是加倍努力,我原来的设计实际上变得更糟。然而,与其在这条错误的道路上继续前进,不如让我们简单地继续研究一个更好的方法。
6.4.3 An Improved Display Design
Early in the discussion of the poor display design, I pointed out that the fatal mistake came from assuming that a unified display necessitated a single class design. However, as we’ve seen, this assumption was invalid. The emergence of state in the calculator implies the need for at least two classes, one for the visual display and one for the state.
在讨论糟糕的显示设计的早期,我指出,致命的错误来自于假设统一的显示必须有一个单一的类设计。然而,正如我们所看到的,这个假设是无效的。计算器中状态的出现,意味着至少需要两个类,一个用于视觉显示,一个用于状态。
Does this remind you of a pattern we’ve already seen? The GUI needs to maintain an internal state (a model). We’re currently in the midst of designing a display (a view). We have already designed a class, the InputWidget, for accepting input and issuing commands (a controller). Obviously, the GUI itself is nothing more than an embodiment of a familiar pattern, the model-view-controller (MVC). Note that relative to the MVC archetype seen in Figure 2-2 in Chapter 2, the GUI can replace direct communication between the controller and the model with indirect communication. This minor change, which promotes decreased coupling, is facilitated by Qt’s signals and slots mechanism.
这是否让你想起了我们已经见过的一个模式?GUI需要维护一个内部状态(一个模型)。我们目前正在设计一个显示(一个视图)。我们已经设计了一个类,InputWidget,用于接受输入和发布命令(一个控制器)。很明显,GUI本身只不过是一个熟悉的模式的体现,即模型-视图-控制器(MVC)。请注意,相对于第二章图2-2中的MVC原型,GUI可以用间接通信取代控制器和模型之间的直接通信。这一微小的变化促进了耦合度的降低,Qt的信号和槽机制为其提供了便利。
We now divert our attention to the design of the newly introduced model class. Upon completion of the model, we’ll return to the Display class to finish its now simpler design and interface.
我们现在把注意力转移到新引入的模型类的设计上。在模型完成后,我们将回到显示类,完成它现在更简单的设计和界面。
6.5 The Model
The model class, which I aptly called the GuiModel, is responsible for the state of the GUI. In order to achieve this goal properly, the model must be the sink for all signals that cause the state of the system to change, and it must be the source of all signals indicating that the state of the system has changed. Naturally, the model is also the repository for the state of the system, and it should provide facilities for other components of the GUI to query the model’s state. Let’s look at GuiModel’s interface in Listing 6-3.
这个模型类,我恰当地称之为GuiModel,负责GUI的状态。为了正确地实现这一目标,模型必须是导致系统状态改变的所有信号的汇,而且它必须是表明系统状态已经改变的所有信号的源。当然,模型也是系统状态的存储库,它应该为GUI的其他组件提供查询模型状态的设施。让我们看看清单6-3中GuiModel的接口。
Listing 6-3. The GuiModel Interface
class GuiModel : public QObject {
Q_OBJECT
public:
enum class ShiftState { Unshifted,
Shifted };
struct State { /* discussed below */
};
GuiModel(QObject* parent = nullptr);
~GuiModel();
void stackChanged(const vector<double>& v);
const State& getState() const;
public slots:
// called to toggle the calculator's shift state
void onShift();
// paired to InputWidget's signals
void onCharacterEntered(char c);
void onEnter();
void onBackspace();
void onPlusMinus();
void onCommandEntered(string primaryCmd, string secondaryCmd);
signals:
void modelChanged();
void commandEntered(string s);
void errorDetected(string s);
};
The six slots in the GuiModel class all correspond to signals emitted by the InputWidget class. The GuiModel interprets these requests, changes the internal state as appropriate, and emits one or more of its own signals. Of particular note is the commandEntered() signal. Whereas the GuiModel’s onCommandEntered() slot accepts two arguments, the raw primary and secondary commands corresponding to the CommandButton that was pressed, the GuiModel is responsible for interpreting the shifted state of the GUI and only re-emitting a commandEntered() signal with the active command.
GuiModel类中的六个槽都对应于InputWidget类所发出的信号。GuiModel解释这些请求,适当地改变内部状态,并发出一个或多个自己的信号。特别值得注意的是commandEntered()信号。GuiModel的onCommandEntered()槽接受两个参数,即对应于被按下的CommandButton的原始主要和次要命令,而GuiModel负责解释GUI的转移状态,并且只重新发射一个带有活动命令的commandEntered()信号。
The remainder of the GuiModel interface involves the GUI’s state. We begin by discussing the rationale behind the nested State struct. Rather than declare each piece of the model’s state as a separate member within GuiModel, I find it much cleaner to lump all of the state parameters into one struct. This design facilitates the querying of the model’s state by permitting the entire system state to be returned by const reference with one function call as opposed to requiring piecemeal access to individual state members. I chose to nest the State struct because it is an intrinsic part of GuiModel that serves no standalone purpose. Therefore, the State struct naturally belongs in GuiModel’s scope, but its declaration must be publicly declared in order for other components of the GUI to be able to query the state.
GuiModel接口的其余部分涉及GUI的状态。我们首先讨论嵌套的状态结构背后的原理。与其在 GuiModel 中把模型状态的每一个部分作为单独的成员来声明,我发现把所有的状态参数放在一个结构中要干净得多。这种设计方便了模型状态的查询,因为它允许整个系统状态通过常量引用返回,只需调用一个函数即可,而不需要零散地访问各个状态成员。我选择嵌套State结构是因为它是GuiModel内在的一部分,没有独立的作用。因此,State 结构自然属于 GuiModel 的范围,但它的声明必须被公开声明,以便 GUI 的其他组件能够查询状态。
The constituents of the State struct define the entire state of the GUI. In particular, this State struct comprises a data structure holding a copy of the maximum number of visible numbers on the stack, the current input buffer, an enumeration defining the shift state of the system, and a Qt enumeration defining the validity of the input buffer. The declaration is shown in Listing 6-4.
State结构的组成部分定义了GUI的整个状态。特别是,这个State结构包括一个数据结构,它保存了堆栈上最大可见数字的副本、当前的输入缓冲区、一个定义系统移位状态的枚举,以及一个定义输入缓冲区有效性的Qt枚举。该声明在清单6-4中显示。
Listing 6-4. The State struct for the GuiModel
struct State {
vector<double> curStack;
string curInput;
ShiftState shiftState;
QValidator::State curInputValidity;
};
An interesting question to ask is, why does the GuiModel’s State buffer the visible numbers from the top of the stack? Given that the Stack class is a singleton, the Display could access the Stack directly. However, the Display only observes changes in the GuiModel (via the modelChanged() slot). Because state changes unrelated to stack changes occur frequently in the GUI (e.g., character entry), the Display would be forced to wastefully query the Stack on every modelChanged() event since the Display is not a direct observer of the stackChanged() event. On the other hand, the GuiModel is an observer of the stackChanged() event (indirectly via a function call from the MainWindow). Therefore, the efficient solution is to have the GuiModel update a stack buffer only when the calculator’s stack actually changes and give the Display class access to this buffer, which is guaranteed by construction to be current, for updating the screen.
一个有趣的问题是,为什么GuiModel的State要从堆栈的顶部缓冲可见的数字?鉴于Stack类是一个单子,Display可以直接访问Stack。然而,Display只观察GuiModel的变化(通过modelChanged()槽)。因为与堆栈变化无关的状态变化经常发生在GUI中(例如,字符输入),显示器将被迫在每个modelChanged()事件上浪费地查询堆栈,因为显示器不是堆栈Changed()事件的直接观察者。另一方面,GuiModel是stackChanged()事件的观察者(通过MainWindow的一个函数调用间接地)。因此,有效的解决方案是让 GuiModel 只在计算器的堆栈实际发生变化时更新一个堆栈缓冲区,并让 Display 类访问这个缓冲区,这个缓冲区通过构造保证是当前的,用于更新屏幕。
6.6 The Display Redux
We are now ready to return our attention to the Display class. Having placed all of the state and state interactions in the GuiModel class, the Display class can be reduced simply to an object that watches for model changes and displays the current state of the calculator on the screen. Other than the constructor, the interface for the Display class consists of only two functions: the slot to be called when the model changes, and a member function to be called to show messages in the status area. The latter function call is used to display errors detected within the GUI (e.g., invalid input) as well as errors detected in the command dispatcher (as transmitted via UserInterface’s postMessage()). The entire interface for the Display class is given in Listing 6-5.
我们现在准备把注意力放回到显示类上。在将所有的状态和状态交互放在GuiModel类中后,Display类可以简单地简化为一个观察模型变化并在屏幕上显示计算器当前状态的对象。除了构造函数外,显示类的接口只包括两个函数:当模型变化时调用的槽,以及在状态区显示信息时调用的成员函数。后一个函数调用用于显示在GUI中检测到的错误(例如,无效输入)以及在命令调度器中检测到的错误(通过UserInterface的postMessage()传送)。显示类的整个界面在清单6-5中给出。
Listing 6-5. The Display Class Interface
class Display : public QWidget {
Q_OBJECT
public:
explicit Display(const GuiModel& g, QWidget* parent = nullptr, int nLinesStack = 6, int minCharWide = 25);
void showMessage(const string& m);
public slots:
void onModelChanged();
};
The optional arguments to the Display class’s constructor simply dictate visual appearance of the stack on the screen. Specifically, a client of the Display class has flexibility over the number of stack lines to display and the minimum width (in units of fixed width font characters) of the on screen display.
显示类构造函数的可选参数只是决定了屏幕上堆栈的视觉外观。具体来说,显示类的客户可以灵活地选择显示的堆栈行数和屏幕上显示的最小宽度(以固定宽度的字体字符为单位)。
6.7 Tying It Together: The Main Window
The main window is a fairly small class that serves a big purpose. To be precise, it serves three purposes in our application. First, as in most Qt-based GUIs, we need to provide a class that publicly inherits from QMainWindow that acts, naturally, as the main GUI window for the application. In particular, this is the class that is instantiated and shown in the function that launches the GUI. Following my typical creative naming style, I called this class the MainWindow. Second, the MainWindow serves as the interface class for the view module of the calculator. That is, the MainWindow also must publicly inherit from our abstract UserInterface class. Finally, the MainWindow class owns all of the previously discussed GUI components and glues these components together as necessary. For all practical purposes, gluing components together simply entails connecting signals to their corresponding slots. These straightforward implementation details can be found in the MainWindow.cpp source code file. We’ll spend the remainder of this section discussing the MainWindow’s design and interface.
主窗口是一个相当小的类,但有很大的用途。准确地说,它在我们的应用程序中起到三个作用。首先,在大多数基于Qt的GUI中,我们需要提供一个公开继承自QMainWindow的类,它自然地充当了应用程序的主GUI窗口。特别是,这是一个被实例化的类,在启动GUI的函数中显示。按照我典型的创造性的命名风格,我把这个类称为MainWindow。其次,MainWindow作为计算器的视图模块的接口类。也就是说,MainWindow也必须公开继承于我们的抽象的UserInterface类。最后,MainWindow类拥有之前讨论过的所有GUI组件,并在必要时将这些组件粘在一起。为了所有的实际目的,将组件粘在一起只需要将信号连接到它们相应的槽上。这些直接的实现细节可以在MainWindow.cpp源代码文件中找到。我们将在本节的剩余部分讨论MainWindow的设计和界面。
We’ve written a Qt application; it’s obvious that we’ll have a descendant of QMainWindow somewhere. That, in and of itself, is not terribly interesting. What is interesting, however, is the decision to use multiple inheritance to make the same class also serve as the UserInterface to the rest of pdCalc. That said, is that truly an interesting decision, or does it just seem provocative because some developers have a moral aversion to multiple inheritance?
我们已经写了一个Qt应用程序;很明显,我们会在某个地方有一个QMainWindow的后裔。这本身并不十分有趣。然而,有趣的是,我们决定使用多重继承来使同一个类同时成为pdCalc其他部分的用户接口。也就是说,这确实是一个有趣的决定,还是因为一些开发者对多重继承有道德上的厌恶而显得具有挑衅性?
Indeed, I could have separated the QMainWindow and the UserInterface into two separate classes. In a GUI where the main window was decorated with menus, toolbars, and multiple underlying widgets, I perhaps would have separated the two. However, in our GUI, the QMainWindow base serves no purpose other than to provide an entry point for our Qt application. The MainWindow literally does nothing else in its QMainWindow role. To therefore create a separate MainWindow class with the sole purpose of containing a concrete specialization of a UserInterface class serves no purpose other than to avoid multiple inheritance. While some may disagree, I think a lack of multiple inheritance, in this instance, would actually complicate the design.
事实上,我本可以将QMainWindow和UserInterface分成两个独立的类。在一个主窗口被菜单、工具栏和多个底层部件装饰的GUI中,我也许会将两者分开。然而,在我们的GUI中,QMainWindow基类除了为我们的Qt应用程序提供一个入口点外,没有任何作用。MainWindow在其QMainWindow的角色中没有做其他事情。因此,创建一个单独的MainWindow类,其唯一的目的是包含UserInterface类的具体特化,除了避免多重继承外,没有其他目的。虽然有些人可能不同意,但我认为在这种情况下,缺乏多重继承实际上会使设计复杂化。
The situation described above is actually an archetypical example of where multiple inheritance is an excellent choice. In particular, multiple inheritance excels in derived classes whose multiple base classes exhibit orthogonal functionality. In our case, one base class serves as the GUI entry point to Qt while the other base class serves as the UserInterface specialization for pdCalc’s GUI view. Notice that neither base class shares functionality, state, methods, or ancestors. Multiple inheritance is especially sensible in situations where at least one of the base classes is purely abstract (a class with no state and only pure virtual functions). The scenario of using multiple inheritance of purely abstract bases is so useful that it is permitted in programming languages that do not otherwise allow multiple inheritance (e.g., interfaces in both C# and Java).
上面描述的情况实际上是一个典型的例子,说明多重继承是一个很好的选择。特别是,多重继承在多个基类表现出正交功能的派生类中表现出色。在我们的例子中,一个基类作为Qt的GUI入口,而另一个基类作为pdCalc的GUI视图的UserInterface专用。请注意,两个基类都没有共享功能、状态、方法或祖先。多重继承在至少有一个基类是纯粹的抽象类(一个没有状态、只有纯虚拟函数的类)的情况下是特别明智的。使用纯抽象基类的多重继承的情况非常有用,以至于在不允许多重继承的编程语言中也允许这样做(例如C#和Java中的接口)。
the two pure virtual functions in the UserInterface class, and a few functions for dynamically adding commands (you’ll encounter these functions in Chapter 7 when we design plugins). For completeness, the interface for MainWindow is shown in Listing 6-6.
UserInterface类中的两个纯虚函数,以及一些用于动态添加命令的函数(在第7章我们设计插件时你会遇到这些函数)。为了完整起见,MainWindow的接口显示在清单6-6中。
Listing 6-6. The Interface for MainWindow
class MainWindow : public QMainWindow, public UserInterface {
class MainWindowImpl;
public:
MainWindow(int argc, char* argv[], QWidget* parent = nullptr);
void postMessage(const string& m) override;
void stackChanged() override;
// plugin functions ...
};
6.8 Look-and-Feel
Before we conclude this chapter with some sample code to execute the GUI, we must return briefly to the final component of the GUI, the LookAndFeel class. The LookAndFeel class simply manages the dynamically customizable appearance of the GUI, such as font sizes and text colors. The interface is simple. For each point of customization, a function exists to return the requested setting. For example, to get the font for the display, we provide the following function:
在我们用一些执行GUI的示例代码来结束本章之前,我们必须简单地回到GUI的最后一个组件,LookAndFeel类。LookAndFeel类只是管理GUI的动态可定制的外观,如字体大小和文本颜色。这个界面很简单。对于每一个定制点,都有一个函数来返回请求的设置。例如,为了获得显示器的字体,我们提供了以下函数。
class LookAndFeel
{
public:
// one function per customizable setting, e.g.,
const QFont& getDisplayFont() const;
// ...
};
Because we only need one LookAndFeel object in the calculator, the class is implemented as a singleton.
因为我们在计算器中只需要一个LookAndFeel对象,该类被实现为一个单例。
A great question to ask is, “Why do we need this class at all?” The answer is that it gives us the opportunity to dynamically modify the appearance of the calculator based on the current environment, and it centralizes in memory access to the look-and-feel of pdCalc. For example, suppose we had wanted to make our GUI DPI aware and choose font sizes accordingly (I didn’t in the source code, but you might want to). With a static configuration file (or, the conceptual equivalent, registry settings), we would have to customize the settings for each platform during the installation process. Either we would have to build customization within the installer for each platform, or we would have to write code to execute during the installation to create the appropriate static configuration file dynamically. If we have to write code, why not just put it in the source where it belongs? As an implementation decision, the LookAndFeel class could be designed simply to read a configuration file and buffer the appearance attributes in memory (a look-and-feel proxy object). That’s the real power of the LookAndFeel class. It centralizes the location of appearance attributes so that only one class needs to be changed to effect global appearance changes. Maybe even more importantly, a LookAndFeel class insulates individual GUI components from the implementation details defining how the GUI discovers (and possibly adapts to) the settings on a particular platform.
一个很好的问题是,“为什么我们需要这个类?” 答案是,它给了我们机会根据当前环境动态地修改计算器的外观,而且它集中了内存中对pdCalc的外观和感觉的访问。例如,假设我们想让我们的GUI具有DPI意识并相应地选择字体大小(我在源代码中没有这样做,但你可能想这样做)。如果有一个静态的配置文件(或者在概念上等同于注册表设置),我们将不得不在安装过程中为每个平台定制设置。要么我们必须在安装程序中为每个平台建立自定义设置,要么我们必须编写代码在安装过程中执行,以动态地创建适当的静态配置文件。如果我们必须写代码,为什么不直接把它放在属于它的源代码中?作为一个实现的决定,LookAndFeel类可以被设计成简单地读取一个配置文件并在内存中缓冲外观属性(一个外观代理对象)。这就是LookAndFeel类的真正力量。它集中了外观属性的位置,所以只需要改变一个类就可以实现全局的外观变化。也许更重要的是,LookAndFeel类将单个GUI组件与定义GUI如何发现(并可能适应)特定平台上的设置的实现细节隔离开。
The full implementation for the LookAndFeel class can be found in the in LookAndFeel.cpp file. The current implementation is very simple. The LookAndFeel class provides a mechanism for standardizing the GUI’s look-and-feel, but no implementation exists to allow user customization of the application. Chapter 8 briefly suggests some possible extensions one could make to the LookAndFeel class to make pdCalc user customizable.
LookAndFeel类的完整实现可以在LookAndFeel.cpp文件中找到。目前的实现是非常简单的。LookAndFeel类提供了一种机制来规范GUI的外观和感觉,但没有任何实现允许用户定制应用程序。第8章简要地提出了一些可能的扩展,我们可以对LookAndFeel类进行扩展,使pdCalc可以被用户自定义。
6.9 A Working Program
We conclude this chapter with a working main() function for launching the GUI. Due to additional requirements you’ll encounter in Chapter 7, the actual main() function for pdCalc is more complicated than the one listed below. However, the simplified version is worth listing to illustrate how to tie pdCalc’s components together with the GUI to create a functioning, standalone executable. See Listing 6-7.
我们以一个用于启动图形用户界面的main()函数结束本章。由于在第七章中你会遇到额外的要求,pdCalc的实际main()函数要比下面列出的函数更复杂。然而,简化的版本值得列出,以说明如何将pdCalc的组件和GUI联系在一起,以创建一个正常的、独立的可执行文件。参见清单6-7。
Listing 6-7. A Working main() Function
int main(int argc, char* argv[])
{
QApplication app { argc, argv };
MainWindow gui { argc, argv };
CommandDispatcher ce { gui };
RegisterCoreCommands(gui);
gui.attach(UserInterface::CommandEntered,
make_unique<CommandIssuedObserver>(ce));
Stack::Instance().attach(Stack::StackChanged,
make_unique<StackUpdatedObserver>(gui));
gui.setupFinalButtons();
gui.show();
gui.fixSize();
return app.exec();
}
Note the similarities between the main() function for executing the GUI above and the main() function for executing the CLI listed at the conclusion of Chapter 5. The likenesses are not accidental and are the result of pdCalc’s modular design.
请注意上面执行GUI的main()函数和第五章末尾列出的执行CLI的main()函数之间的相似之处。这些相似之处不是偶然的,是pdCalc模块化设计的结果。
As with the CLI, to get you started quickly, a project is included in the repository source code that builds an executable, pdCalc-simple-gui, using the above main() function as the application’s driver. The executable is a standalone GUI that includes all of the features discussed up to this point in the book.
和CLI一样,为了让你快速入门,在资源库的源代码中包含了一个项目,它使用上面的main()函数作为应用程序的驱动,构建了一个可执行文件,pdCalc-simple-gui。该可执行文件是一个独立的GUI,包括本书到此为止讨论的所有功能。
Before concluding this section, I’ll make a few comments about the implementation above. First, the QApplication class, calling show() on the gui, and the app.exec() call are all boilerplate Qt code. As it concerns us here, those calls simply enable us to start up the GUI and show it on the screen. Second, setupFinalButtons() is called on the gui, but we never defined this function as part of MainWindow’s interface. This function is needed to add the buttons correctly in the presence of plugins. With the standalone GUI designed in this chapter, the setupFinalButtons() function is unnecessary. I included this function in the code listing above so that the main() function above could be used as-is with the existing GitHub repository code. Finally, the fixSize() function is also not included in the interface built in this chapter. This function is an implementation detail and contributes nothing to the GUI’s design. The function is simply used to fix the size of the GUI on screen and remove the ability to resize it. Again, the necessity of this function arises due to plugins because we can only know the final geometry of the GUI after plugins have added their buttons.
在结束本节之前,我想对上面的实现做一些评论。首先,QApplication类,在gui上调用show(),以及app.exec()调用都是Qt的模板代码。就我们这里而言,这些调用只是使我们能够启动GUI并在屏幕上显示它。第二,setupFinalButtons()在GUI上被调用,但我们从未将这个函数定义为MainWindow界面的一部分。在有插件的情况下,需要这个函数来正确添加按钮。在本章设计的独立GUI中,setupFinalButtons()函数是不必要的。我在上面的代码列表中加入了这个函数,这样上面的main()函数就可以按现有的GitHub仓库代码来使用了。最后,fixSize()函数也没有包含在本章构建的界面中。这个函数是一个实现细节,对GUI的设计没有任何贡献。该函数只是用来固定GUI在屏幕上的大小,并删除了调整其大小的能力。同样,这个函数的必要性是由于插件而产生的,因为我们只有在插件添加了它们的按钮之后才能知道GUI的最终几何形状。
6.10 A Microsoft Windows Build Note
pdCalc is designed to be both a GUI and a CLI. In Linux, no compile time distinction exists between a console application (CLI) and a windowed application (GUI). A unified application can be compiled with the same build flags for both styles. In Microsoft Windows, however, creating an application that behaves as both a CLI and a GUI is not quite as trivial because the operating system requires an application to declare during compilation the usage of either the console or the Windows subsystem.
pdCalc被设计成既是GUI又是CLI。在Linux中,控制台应用程序(CLI)和窗口应用程序(GUI)之间不存在编译时的区别。一个统一的应用程序可以用相同的编译标志来编译两种风格。然而,在Microsoft Windows中,创建一个既能作为CLI又能作为GUI的应用程序并不那么简单,因为操作系统要求应用程序在编译时声明使用控制台或Windows子系统。
Why does the declaration of the subsystem matter on Windows? If an application is declared to be a windowed application, if it is launched from a command prompt, the application will simply return with no output (i.e., the application will appear as if it never executed). However, when the application’s icon is double-clicked, the application launches without a background console. On the other hand, if an application is declared to be a console application, the GUI will appear when launched from a command prompt, but the GUI will launch with a background console if opened by double-clicking the application’s icon.
为什么子系统的声明在Windows上很重要?如果一个应用程序被声明为窗口应用程序,如果它从命令提示符启动,该应用程序将简单地返回,没有输出(即,该应用程序将显示为从未执行)。然而,当应用程序的图标被双击时,应用程序的启动没有背景控制台。另一方面,如果一个应用程序被声明为一个控制台应用程序,当从命令提示符启动时,GUI将出现,但如果通过双击应用程序的图标打开,GUI将以背景控制台启动。
Conventionally, Microsoft Windows applications are designed for one subsystem or the other. In the few instances where applications are developed with both a GUI and a CLI, developers have created techniques to avoid the above problem. One such technique creates two applications, a .com and an .exe, that the operating system can appropriately call depending on the option selected via command line arguments.
传统上,微软Windows应用程序是为一个子系统或另一个子系统设计的。在少数情况下,如果开发的应用程序同时具有GUI和CLI,开发人员就会创造一些技术来避免上述问题。其中一种技术创建了两个应用程序,一个是.com,一个是.exe,操作系统可以根据通过命令行参数选择的选项适当地调用它们。
In order to keep pdCalc’s code simple and cross platform, I ignored this problem and simply built the GUI in the console mode (pdCalc-simple-gui, however, having no CLI, is built in windowed mode). Indeed, this means that if the application is launched by doubleclicking pdCalc’s icon, an extra console window will appear in the background. If you intend to use the application primarily as a GUI, the problem can be remedied by simply removing the ability to use the console (e.g., comment out the line win32:CONFIG += console in the pdCalc.pro build file). If you need access to both the CLI and the GUI and the extraneous console drives you crazy, you have two realistic options. First, search the Internet for one of the techniques discussed above and give it a try. Personally, I’ve never gone that route. Second, build two separate executables (maybe called pdCalc and pdCalc-cli) instead of one executable capable of switching modes based on command line arguments. The application’s flexible architecture trivially supports either decision.
为了保持pdCalc的代码简单和跨平台,我忽略了这个问题,只是在控制台模式下构建GUI(pdCalc-simple-gui,然而,没有CLI,是在窗口模式下构建的)。事实上,这意味着如果通过双击pdCalc的图标来启动应用程序,一个额外的控制台窗口会在后台出现。如果你打算把这个程序主要作为GUI使用,这个问题可以通过简单的移除使用控制台的能力来解决(例如,在pdCalc.pro构建文件中注释掉win32:CONFIG += console这一行)。如果你需要同时访问CLI和GUI,而不相干的控制台让你发疯,你有两个现实的选择。首先,在互联网上搜索上面讨论的技术之一,并试一试。就个人而言,我从未走过这条路。第二,建立两个独立的可执行文件(也许叫pdCalc和pdCalc-cli),而不是一个能够基于命令行参数切换模式的可执行文件。该应用程序的灵活架构可以支持这两种决定。