My simpledisplay.d
file is a module to create and interact with a simple window, enabling easy graphics display. On Windows, it wraps the Win32 GDI API, and on other systems it uses Xlib for broad compatibility with minimal dependencies.
Let's execute the following steps to create a graphics window:
simpledisplay
.SimpleWindow
object with the image's size.window.eventLoop
.25
as the first argument to eventLoop
to request a 25 millisecond timer.window.draw
, and draw the image to it with painter.drawImage
.KeyEvent
argument to serve as a key handler. Call window.close
to close the window and exit the loop when you receive the key to exit.pause
flag in the key handler.dmd yourfile.d simpledisplay.d color.d
.The code is as follows:
import simpledisplay; void main() { bool paused; auto image = new Image(256, 256); auto window = new SimpleWindow(image.width, image.height); window.eventLoop(25, () { if(paused) return; import std.random; foreach(y; 0 .. image.height) foreach(x; 0 .. image.width) image.putPixel(x, y, uniform(0, 2) ? Color.black : Color.white); auto painter = window.draw(); painter.drawImage(Point(0, 0), image); }, (KeyEvent ke) { if(ke.key == Key.Space) paused = !paused; if(ke.key == Key.Escape) window.close(); } ); }
Running the program will pop up a window with random white noise, similar to static on an old TV set, as shown in the following screenshot:
As you hold the Space bar, the animation will temporarily stop, and releasing the Space bar will cause it to resume.
The simpledisplay.d
module includes the necessary operating system function bindings and some cross-platform wrapping to enable the easy creation of a graphics window with no outside dependencies except color.d
. It doesn't even use Phobos. You may notice improved compile times because neither file imports the standard library.
The implementation uses mixin
templates to provide the different code for each platform. If I write this again, I will not use this approach—it resulted in code duplication and difficulty in maintenance. However, this approach does have one potential saving grace: it can be used to separate the operating system implementations into entirely different modules, which may become more maintainable. I didn't do this because I wanted the file to just work when downloaded individually. The color.d
module eventually became a requirement—without it, the image modules and windowing module will both need to declare separate structures to hold a color, leading to silly incompatibilities between them. Even if two structures in different modules have exactly the same layout, they are considered separate, incompatible types (unless you use an adapter or a cast
). However, while the ideal of one standalone file did not work, I still wanted to stay as close to it as possible and opted for one large simpledisplay.d
file that covers all platforms.
Another challenge of the implementation was mapping window procedures of the operating system level back to the SimpleWindow
object of D's class. The window procedures must match a specific signature and calling convention to be callable by the operating system (or, in the case of X11, the event loop must work with window IDs from the X server, which similarly leaves no room for the injection of a D class).
To solve this, I used an associative array of window handle/ID to window class mappings. The constructor of SimpleWindow
creates the window and stores the native window handle in a static associative array that leads back to itself. Similarly, the destructor of SimpleWindow
removes itself from this map. When a window handle comes through the event loop, the associative array is used to get the class instance and forward the message to it to be processed.
The event loop of SimpleWindow
uses an array of delegates that are matched on argument types, inspired by std.concurrency
. This is implemented with a foreach
loop and __traits(compiles)
; it loops over its arguments and attempts to assign them to the event handling delegates. If it succeeds, it accepts the handler. If not, a static assert is used to issue a compile-time error.
Finally, simpledisplay.d
also supports the following two forms of drawing:
To draw to the window, a draw
method that returns a Painter
struct is used. The painter retains a reference to the window for use with native API functions and has a destructor to automatically swap buffers and clean up resources when it goes out of scope.
Do not use draw
before entering your event loop unless you surround the code in braces to ensure the painter goes out of scope before entering the loop. Otherwise, its destructor will not fire until program termination!
To draw directly to an image, the implementation tries to achieve efficiency by using a final
class and shared memory on X11, when available. The final
class provides a ~10 percent speed boost over a regular class because it contains no virtual functions, which can be a performance bottleneck on modern CPUs. The shared memory obviates image copying to and from the display server on the network-transparent X protocol, saving a significant amount of time. All this is done transparently to the user.
An interesting consequence of the shared memory approach, however, is the requirement to ensure cleanup tasks are always performed. As the resource is shared across processes, the operating system will not automatically release the shared memory handles when your program terminates. Thus, a signal handler must be installed to handle SIGINT when the user presses Ctrl + C. This is the same situation that was discussed in the Getting real-time input from the terminal recipe. If you fail to do this, it may make your X server difficult to use because the number of possible shared memory handles is limited.
18.118.20.68