Welcome to this series of three articles about developing a real-world, professional drawing editor with Java. We will discuss design choices and the implementation of the many possible features. I will discuss the most important issues, keeping the number of classes, and the amount of code to the minimum possible, in order to focus effectively on design concepts.
Whoever says that Java is too clumsy for the desktop should try out this simple piece of software. The first objective of the series is to show better design techniques; as a bonus, though, you will get a full working drawing editor that features, among other things:
- Ease of expandability with maximum flexibility:
- A simple mechanism for adding new symbols to the palette, even at runtime (plus several other features like serialization of the symbol palette)
- Each symbol has its own properties and can support its own actions
- Undo support
- Load and save functions
- Print function
- Common functionalities such as cut, paste, rotate, etc.
- Coherent enabling/disabling of menu items and toolbar buttons, depending on the user’s input
- An expandable architecture that easily allows sophisticated features like zooming
In Figure 1, we have a screenshot from the drawing editor.
Symbol Creation — Some Theory
We start our series with the symbol hierarchy design; in the second part, we will see the overall software architecture and discuss its most compelling aspects.
The Abstract Factory Design Pattern
There are many cases where the simple creation of objects represents a design problem itself. The classic Factory Method design pattern needs an abstract creator class so that its concrete subclasses can instantiate the needed Product subclasses. (See Figure 2.)
In some situations, the classic diagram below can be simplified. When the information about which product to create is given from outside to the Factory class, the subclasses of Creator are not needed. In our case, the information about what symbol (product) to create is given by the user, who selects it from a palette that stores all the known symbols.
We can translate our diagram to the following one (shown in Figure 3), tailored to our drawing editor:
The
ConcreteProduct |
AbstractSymbol |
Note that we don’t need any special code for the initialization of the concrete subclasses of
AbstractSymbol |
Creator |
AbstractSymbol |
Creator |
First of all, we have to define a hierarchy of symbols useful for our editor. Indeed, we may have several hierarchies, depending on the kind of domain our symbols belong to (geometrical shapes rather than technical symbols, etc.), the implementation chosen (splines, etc.) efficiency and other constraints. Of course any symbol hierarchy (also known as an ontology) is partial and arbitrary, in that its main required feature is to serve our purposes (see Figure 5). All the instances of these classes represent the objects put by the user on the drawing canvas. These objects can be dragged with the mouse, opened by the user to be modified, saved in a stream to disk, and so on.
One key guideline here is to keep all the behavior of one object inside itself (nothing new from an OO viewpoint). This means that when a symbol is edited, only its class is responsible for its behavior. A line will show the control points so they can be modified by the user (see Figure 4.). A text object will prompt the user with a caret to modify the displayed text via keyboard. The container and the other classes don’t know anything about what is actually going on inside the symbol. This provides many useful properties, such as the ability to modify the behavior of single objects, or to add new symbols to the palette. Whenever an object needs to communicate with the outside framework, an event mechanism will do the job without sacrificing the decoupling we obtained with the architecture.
Parameterized Factory Method
The plot gets thicker when our drawing editor needs to deal with potentially hundreds of different types of symbols. Maintainability and efficiency issues must be taken into account. One common-sense guideline is to keep to the smallest number of classes possible. The internal code and the external behavior for, say, the pentagon and hexagon classes are very close, and they can safely be kept in one class. We just have to manage vertices, and let them be edited by the user, and so forth. On the other hand, we want to keep them different, because they have a conceptual meaning, and not just a different number of vertex in the implementation. To avoid letting the number of subclasses of
AbstractSymbol |
We must design the
AbstractSymbol |
Designing the AbstractSymbol Class
We want the user to be able to cut, copy, paste, scale, rotate and delete every symbol. In addition to having these functionalities, every symbol must fit within our software framework. It must be drawn onto the screen, so a symbol has to have attributes for the pixel location (the point that refers to the upper-left pixel of the symbol). Because we can generally modify the symbols (for instance, change the control points of a line, see Figure 4), a Boolean property that indicates the edit state of the symbol is needed. Of course, because every symbol can be edited to some extent, we only write the code once for this operation in the
AbstractSymbol |
Implemented Classes
In this version the following graphical symbols are implemented (and given the prices paid for it, it’s a good deal):
- line
- curve
- ellipse
- bitmap icon
Let’s have a look at the
AbstractSymbol |
- We want to save and load our symbols (we can use standard Java serialization to accomplish this.)
- We would like to clone symbols, for example, to keep track of the operations performed by the user, in order to provide undo support.
- Symbol Factory: We need a way to build symbols when the user chooses them from the palette. Here we have many possible equally good design choices. For example, one can centralize the symbol creation in a
class. We could do it using an integer enumeration-like value, but it wouldn’t scale nicely, and it’s always tricky to fix things with dynamic features. Because today’s Java lacks the feature of parameterized types (i.e., parameterized classes), we need a simple and effective ad hoc workaround. The needed behavior is to create the rightSymbolFactory
subclass. A better and simpler way is just letting any symbol be responsible for creating new instances of itself. There are several points to notice here:AbstractSymbol
- We are modeling the specialized behavior of creating a new symbol to put on the canvas. In some situations, that could be different from plain creation, by means of a standard constructor, for example. Distinguishing it from standard instantiation adds cleanness and clarity.
- We lose centralization, and that could be an issue, especially for large ontologies and/or complex symbol environments (for example, if we were building a complex graph drawing toolkit, centralization could help both to tame complexity and to offer some extra features needed for complex symbol environments such as topology layout, etc.).
- We gain in simplicity, but also in redundancy; have a look at the
source code
. - Finally, because it’s an unusual choice. I think that seeing and discussing unusual design options can help sometimes to broaden the view, instead of always feeding developers with ready-made solutions.
Unfortunately, we don’t have the space to go into these aspects deeply here. My choice is toward cleanness and simplicity. Yours could be dynamic instantiation of classes from the palette or default constructors, cloning as in the prototype design pattern, etc. As you like. The interface
allows the creation of new symbols.SymbolCreable
- Every symbol could perform its own set of actions, callable from the right mouse button, for example. The simplest way to do that is implementing the
interface. In our simple editor, it’s not implemented.ActionListener
- Symbols not only show the user commands but properties, as well. For example, a bitmap icon has an image as a property, and we would like the user to change it as needed. The interface
will model this aspect. (Note that not onlyHavingUserProperties
s will need that behavior.) The simplest way is to prompt the user with a dialog containing the symbol’s user-sensitive properties. Again, every symbol is only responsible for its own properties management.AbstractSymbol
- A lot of different behaviors could be added, depending on the complexity of our task. A common enhancement is a set of event listeners and event producers for every symbol, as in bean-like environments.
That said, we have the following class inheritance for our simple editor:
public abstract class AbstractSymbol implements java.io.Serializable, Cloneable, SymbolCreable, HavingUserProperties { …
Then the methods:
public boolean contains(Point p, int tolerance)
Whether a screen point lies or not in the symbol.
public abstract void draw(java.awt.Graphics2D g)
Mimics the
method for rendering itself onto the canvas.paint()
public abstract Rectangle2D.Float getRectBounds()
To obtain the rectangle bounding the symbol; a rough design choice. A finer solution would be to obtain a shape rather than a rectangle.
public Object clone()
Needed for cloning symbols.
public void processMouseEvent(MouseEvent me)
A very important method. Allows responsibility, encapsulation, and flexibility at the same time. Symbols receive mouse events from their container to be processed internally. For example, adding a control point to a curve is handled by this method.
public abstract AbstractSymbol createNew()
Implements
. Creates a new symbol.SymbolCreable
public void initializeAt(Point location)
A method for setting up a symbol on the screen at a given location.
public abstract void editProperties()
Implements
. Forces the symbol to show a JDialog in order to modify its internal settings, as color, size, etc.HavingUserProperties
public void rotate()
Rotates the symbol of a given, fixed amount (i.e., it draws the symbol rotating it accordingly).
public void rotateBack()
Produces the opposite effect of the previous method.
And the properties:
boolean editMode
Whether the symbol is open in edit mode (i.e., shows its control points, etc.) or not. Note that some symbols do not have an edit state, like bitmap icons, for instance.
float rotationAngle
The current rotation angle, affected by the two methods,
androtate()
.rotateBack()
boolean selected
Whether the symbol is selected or not. Depending on this flag, the
method changes the appearance of the symbol to be drawn.draw()
Point location
The symbol location on the screen.
The Static Class Diagram
Finally, we can look at the classes we discussed in this first part, shown in Figure 6.
In the next article, we will explore the rest of the software architecture, and in the third part we will put all the pieces together.
About the Author
Mauro Marinilli has been a Java developer since the language’s early days. He’s also active in academic research, mainly in information filtering, case-based reasoning, and adaptive hypermedia. He is currently working as chief engineer for the GUI team developing the new Italian Air Force Meteorological and Forecasting System.