http://www.developer.com/

Back to article

A Java Drawing Editor, Part 1: Creation Aspects


January 3, 2001

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.

Figure 1. 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.

Figure 2.

We can translate our diagram to the following one (shown in Figure 3), tailored to our drawing editor:

Figure 3.

The

ConcreteProduct
class represents a placeholder for all the concrete subclasses of an
AbstractSymbol
, which we will examine below.

Note that we don't need any special code for the initialization of the concrete subclasses of

AbstractSymbol
, because we have to create an object with internal default values and place it on the screen where the cursor is. This means that our
Creator
class doesn't need to be subclassed for every
AbstractSymbol
's concrete subclass; we have just one concrete
Creator
class for all our symbols. We accomplish this by creating symbols taken from the palette and adding them to the canvas as needed.

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.

Figure 4.

Figure 5.

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
explode, and in order not to have just a few large and unmanageable classes that draw everything, we can simply use a parameter to detail the (sub)type that the given class implements. So when the user selects a pentagon from the symbol palette, the software creates a polyhedron instance obtained with the "pentagon" parameter given to the constructor. A possible implementation could be using a hash table for mapping subtypes with actual classes, so that we can trace back symbols to the classes that actually implement them. To keep our implementation simple, we won't use this feature, although it is essential in large symbol ontologies.

We must design the

AbstractSymbol
class from two different perspectives: one internal (toward the canvas object and the other components of our software architecture), and the other external (toward the user).

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
class, letting the subclasses specialize it for more complex behavior.

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
class: First of all, let's start from the intended behaviors for our symbols:

  • 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
    SymbolFactory
    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 right
    AbstractSymbol
    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:
    • 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
    SymbolCreable
    allows the creation of new symbols.
  • 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
    ActionListener
    interface. In our simple editor, it's not implemented.
  • 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
    HavingUserProperties
    will model this aspect. (Note that not only
    AbstractSymbol
    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.
  • 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
    paint()
    method for rendering itself onto the canvas.
  • 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
    SymbolCreable
    . Creates a new symbol.
  • public void initializeAt(Point location) 

    A method for setting up a symbol on the screen at a given location.
  • public abstract void editProperties()

    Implements
    HavingUserProperties
    . Forces the symbol to show a JDialog in order to modify its internal settings, as color, size, etc.
  • 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,
    rotate()
    and
    rotateBack()
    .
  • boolean selected

    Whether the symbol is selected or not. Depending on this flag, the
    draw()
    method changes the appearance of the symbol to be drawn.
  • 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.

Figure 6. The classes related to our chosen symbol ontology.

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.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date