Writing Your Own X-11 Framework Basics (Part 1)
We go in depth into a X-11 foundational primer.
Many many moons ago a wonderful piece of software came out for Window 95, namely Delphi 2.0. It was amazing, and let you literally draw your buttons and applications, connect them to event handlers, and then insert some boilerplate Turbo Pascal code. Today you would realize this was 25 years ahead of it's time, and basically long buried and gone after many attempts by Windows to bury it as it competed with their myriad of various IDE produtcs. It survived to a basic fashion, and you can still pay huge dollars and buy Embarcadero products that have bought and traded hands many times, or the free Lazarus IDE, which was an opensource clone of the powerful Delphi products.
But as for a C / C++ based type GUI (in Linux) if you are not going the Embarcadero route, you are pretty much stuck mostly with Qt. Qt is really nice, however if you want total control like say a custom sized button that acts like a instrument you end up fighting the layout manager which will decide for you that your button needs to be a minimum of X pixels, or the font needs to be N size. Then the restrictive licensing agreements will helm you in. In the end you waste days getting a generic 'toy' layout you never wanted.
So what if we just wrote one from scratch! Secondly why even write this article when the AI's do it so well.. The answer is really simple to really learn something you need to spend some time in it, not just glancing through code but documenting each part one bit at a time. It could be noted that 'vibe-coding' something to come back to it even a week later and you have no understanding or recollection of how it works, or what it does is just powerless - and dangerous. AI's will get 98% of stuff right, but the 2% that they get wrong will trap you. If you don't have a good understanding and can work close to the level that the AI is working at you will be trapped. That is the goal of the series of these articles to slowly build a strong foundational knowledge of the X-11 X-Window System, and eventually build an entire RAD (Rapid Application Developer) or IDE.
Supports:
Before you can write any X-11 application make sure to reference it in your source, and to also install the support library, so:
sudo apt install libx11-dev -y#include <X11/Xlib.h>
#include <X11/Xutil.h>Here additionally is a working CMakeLists.txt if it helps your project.
cmake_minimum_required(VERSION 3.20)
project(XFrame LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(X11 REQUIRED)
add_executable(XFrame main.cpp)
target_link_libraries(XFrame PRIVATE X11::X11)
BoilerPlate Introduction
int main()
{
Display* display = XOpenDisplay(nullptr);
if (!display) {return 1; }
int screen = DefaultScreen(display);
XID Xid = RootWindow(display, screen);
Window window = XCreateSimpleWindow(display, Xid,100,100,800,600,1, BlackPixel(display, screen), WhitePixel(display, screen));
XStoreName(display, window, "My C++ Canvas Framework");
XSelectInput(display, window, ExposureMask | KeyPressMask | StructureNotifyMask );
Atom deleteWindowMessage = XInternAtom(display, "WM_DELETE_WINDOW", False);
XSetWMProtocols(display,window,&deleteWindowMessage,1);
XMapWindow(display, window);
GC gc = DefaultGC(display, screen);
Canvas canvas(display, window, gc, 800, 600);
bool running = true;
while (running)
{
XEvent event;
XNextEvent(display, &event);
switch (event.type)
{
case Expose:
canvas.present();
break;Going over this snippet of code let's just study the different parts:
Display* display = XOpenDisplay(nullptr);This will get the current display. Please note - this is not necessarily the screen as X-11 can have virtual displays, but will get the active one that the current session is calling to use. So if you open an application inside your Linux Gui, this is the display.if (!display) {return 1;}is simply saying if no display is returned exit with error code 1 versus 0 (no error code.)int screen = DefaultScreen(display);If you have a 3-head monitor setup this will get the default monitor of the group. Typically0XID Xid = RootWindow(display, screen)Effectively this is simply the resource identifier that the curent display / sreen combination is using. When we step-debugged our code we receivedXid: 883Window window = XCreateSimpleWindow(display, Xid,100,100,800,600,1, BlackPixel(display, screen), WhitePixel(display, screen));This actually creates a window, however it will not be displayed just yet. It exists virtually.displaythe current display,Xid- the resource identifier of the display, screen combination,100,100isx,ycoordinates from top-left,800,600is thewidth, height.1is the border width, andBlackPixel(display,screen)border, and aWhitePixel(display,screen)background.XStoreName(display, window, "My C++ Canvas Framework");Will give this (still virtual) window a name for it's title bar.
Event Signal Handling
XSelectInput(display, window, ExposureMask | KeyPressMask | StructureNotifyMask );will determine which events will be passed to the window for handling. An event is - did your mouse move? Does your window need to know about it, then we add a corresponding mask.
Here is a list of possible masks:
The standard event masks available to XSelectInput are:
| Mask | Reports this event |
|---|---|
NoEventMask | No events |
KeyPressMask | A keyboard key is pressed |
KeyReleaseMask | A keyboard key is released |
ButtonPressMask | A mouse button is pressed |
ButtonReleaseMask | A mouse button is released |
EnterWindowMask | The pointer enters the window |
LeaveWindowMask | The pointer leaves the window |
PointerMotionMask | The pointer moves inside the window |
PointerMotionHintMask | Pointer-motion hints instead of every motion event |
Button1MotionMask | Pointer moves while button 1 is held |
Button2MotionMask | Pointer moves while button 2 is held |
Button3MotionMask | Pointer moves while button 3 is held |
Button4MotionMask | Pointer moves while button 4 is held |
Button5MotionMask | Pointer moves while button 5 is held |
ButtonMotionMask | Pointer moves while any mouse button is held |
KeymapStateMask | Keyboard mapping state notification |
ExposureMask | Part or all of the window needs repainting |
VisibilityChangeMask | Window visibility changes |
StructureNotifyMask | This window is moved, resized, mapped, unmapped, or destroyed |
ResizeRedirectMask | Requests notification before this window is resized |
SubstructureNotifyMask | A child window is created, moved, resized, mapped, unmapped, or destroyed |
SubstructureRedirectMask | Requests control over changes to child windows |
FocusChangeMask | Keyboard focus enters or leaves the window |
PropertyChangeMask | A window property changes |
ColormapChangeMask | The window’s colormap changes |
OwnerGrabButtonMask | The window receives button events even when another window has a pointer grab |
Specifically if we want button based event handlers it becomes:
The mouse and pointer-related masks available to XSelectInput are:
| Mask | Event type | Meaning |
|---|---|---|
ButtonPressMask | ButtonPress | A mouse button was pressed |
ButtonReleaseMask | ButtonRelease | A mouse button was released |
EnterWindowMask | EnterNotify | The pointer entered the window |
LeaveWindowMask | LeaveNotify | The pointer left the window |
PointerMotionMask | MotionNotify | The pointer moved |
PointerMotionHintMask | MotionNotify | Requests motion hints instead of every motion event |
Button1MotionMask | MotionNotify | The pointer moved while button 1 was held |
Button2MotionMask | MotionNotify | The pointer moved while button 2 was held |
Button3MotionMask | MotionNotify | The pointer moved while button 3 was held |
Button4MotionMask | MotionNotify | The pointer moved while button 4 was held |
Button5MotionMask | MotionNotify | The pointer moved while button 5 was held |
ButtonMotionMask | MotionNotify | The pointer moved while any button was held |
OwnerGrabButtonMask | Affects pointer grabs | Allows the window to receive button events when another client has a pointer grab |
Typically a normal mouse interaction type mask setting would look like:
XSelectInput(display, window, ExposureMask | KeyPressMask | StructureNotifyMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | EnterWindowMask | LeaveWindowMask
);From this you would then have the following code block to handle these events, namely:
switch (event.type) {
case ButtonPress: {
int x = event.xbutton.x;
int y = event.xbutton.y;
unsigned int button = event.xbutton.button;
// Handle mouse press.
break;
}
case ButtonRelease: {
int x = event.xbutton.x;
int y = event.xbutton.y;
unsigned int button = event.xbutton.button;
// Handle mouse release.
break;
}
case MotionNotify: {
int x = event.xmotion.x;
int y = event.xmotion.y;
// Handle pointer movement.
break;
}
case EnterNotify:
// Pointer entered the window.
break;
case LeaveNotify:
// Pointer left the window.
break;
}
Finishing up this block of code we still have:
Atom deleteWindowMessage = XInternAtom(display, "WM_DELETE_WINDOW", False);
XSetWMProtocols(display,window,&deleteWindowMessage,1);
XMapWindow(display, window);
GC gc = DefaultGC(display, screen);
Canvas canvas(display, window, gc, 800, 600);Atom deleteWindowMessage = XInteralAtom(display, "WM_DELETE_WINDOW", False);This is telling the system to give us it's internalAtomreference of aWM_DELETE_WINDOWmessage as we want to intercept it and handle it manually.XSetWMProtocols(display, window, &deleteWindowMessage, 1);Once we have the referencedAtomwe then use that address to reference back that our application supports theWM_DELETE_WINDOWprotocol.XMapWindow(display, window)will make your application visible.
Picking your Drawing Context (Picking your Pen to Draw With)
Finally - yep I know this is a LOT, but were almost there, we need to get first the drawing context,
- A drawing context is only settings - it describes the pen you are holding in your hand at the time you used it to write.
- For instance you might have one
drawing contextthat uses line width 5 and a second that usesred.Again think of them as pens. - The default drawing context is obtained with :
GC gc = DefaultGC(display, screen);
Drawing Commands
XDrawPoint
XDrawPoint(display, window, gc, 100, 80);XDrawPointOf the currentdisplayfor the currentwindowusing thedrawing contextofgccreate a single point at100,80. This command queues untilxflush(display)is called.
XDrawPoints
XPoint points[] = {{100, 80}, {110, 90}, {120, 100}, {130, 110}};
XDrawPoints(display, window, gc,points, 4, CoordModeOrigin);XDrawPointsOf the currentdisplayfor the currentwindowusing thedrawing contextofgchere are4coordinate points.CoordModeOriginspecifies that each point is absolute, whileCoordModePreviousindicates the first point is absolute, and each point thereafter is relative. This command queues untilxflush(display)is called.
XDrawLine
XDrawLine(display, window, gc,
50, 150, // starting x, y
300, 200 // ending x, y
);
XDrawLineof the currentdisplayfor the currentwindowusing thedrawing contextofgcdraw a line from50,150to300,120This command queues untilxflush(display)is called.
XDrawLines
XPoint linePoints[] = {{50, 250}, {150, 200}, {250, 250}, {350, 200}};
XDrawLines(display, window, gc, linePoints, 4, CoordModeOrigin);XDrawLinesOf the currentdisplayof the currentwindowusing thedrawing contextofgchere are4consecutive points that will will draw contiguously. This command queues untilxflush(display)is called.
XDrawSegments
XSegment segments[] = {
{50, 300, 150, 350},
{200, 300, 300, 350},
{350, 300, 450, 350}
};
XDrawSegments(display, window, gc, segments, 3);XDrawSegmentsOf the currentdisplayof the currentwindowusing thedrawing contextofgchere are3line segments that can each be drawn. This command queues untilxflush(display)is called.
XDrawRectangle
XDrawRectangle(display, window, gc,
50, 400, // x, y
150, 80 // width, height
);XDrawRectangleOf the currentdisplayof the currentwindowusing thedrawing contextofgcdraw a rectangle starting at50,400with a width,height of150,80This command queues untilxflush(display)is called.
XDrawRectangles
XRectangle outlinedRectangles[] = {
{250, 400, 100, 60},
{400, 400, 100, 80},
{550, 400, 150, 50}
};
XDrawRectangles(display, window, gc, outlinedRectangles,3);XDrawRectanglesOf the currentdisplayof the currentwindowusing thedrawing contextofgcdraw the following3rectangles as passed inoutlinedRectanglesThis command queues untilxflush(display)is called.
XFillRectangle
XFillRectangle(
display,
window,
gc,
50, 500, // x, y
150, 60 // width, height
);XFillRectangleOf the currentdisplayof the currentwindowusing thedrawing contextofgcdraw the followingfilled rectangleThis command queues untilxflush(display)is called.- Does not put a line around the rectangle, you must make a second call to
XDrawRectangle
One may set the fill with:
XSetFillStyle(display, gc, FillSolid);
XSetForeground(display, gc, 0xFF0000);
// Four fill styles are offered:
// FillSolid
// FillTiled
// FillStippled
// FillOpaqueStippledXFillRectangles
XRectangle filledRectangles[] = {
{250, 500, 100, 50},
{400, 500, 100, 70},
{550, 500, 150, 40}
};
XFillRectangles(display, window, gc, filledRectangles, 3);
XFillRectanglesOf the currentdisplayof the currentwindowusing thedrawing contextofgcdraw the following set offilled rectanglesThis command queues untilxflush(display)is called.
XDrawArc
- Angles are measured in arc seconds each second is 1/64th of a degree, thus to draw a 1/2 circle becomes
180 * 64
XDrawArc(
display,
window,
gc,
50, 50, // bounding box x, y
150, 100, // bounding box width, height
0, // starting angle
180 * 64 // angle extent
);
XDrawArcOf the currentdisplayof the currentwindowusing thedrawing contextofgcdraw the following arc starting at reference point50, 50and bounded by150,100with a starting angle of0 degrees. This command queues untilxflush(display)is called.
XDrawArcs
- Angles are measured in arc seconds each second is 1/64th of a degree, thus to draw a 1/2 circle becomes
180 * 64
XArc outlinedArcs[] = {
{250, 50, 100, 100, 0, 360 * 64}, // circle
{400, 50, 100, 100, 0, 180 * 64}, // half-circle
{550, 50, 150, 100, 90 * 64, 180 * 64}
};
XDrawArcs(display, window, gc,outlinedArcs, 3);XDrawArcsOf the currentdisplayof the currentwindowusing thedrawing contextofgcdraw the following3arcs each referenced byx, y, width, height, starting angle, angle extentThus each arc is described by 6 points of data. This command queues untilxflush(display)is called.
XFillArc
- Angles are measured in arc seconds each second is 1/64th of a degree, thus to draw a 1/2 circle becomes
180 * 64
XFillArc(
display,
window,
gc,
50, 180, // bounding box x, y
150, 150, // width, height
0, // starting angle
90 * 64 // angle extent
);
XFillArcOf the currentdisplayof the currentwindowusing thedrawing contextofgcdraw and fill in the following arc referenced byx, y, width, height, starting angle, angle extentThus each arc is described by 6 points of data. This command queues untilxflush(display)is called.
XFillArcs
XArc filledArcs[] = {
{250, 180, 100, 100, 0, 360 * 64}, // filled circle
{400, 180, 100, 100, 0, 180 * 64}, // filled half-circle
{550, 180, 150, 100, 90 * 64, 180 * 64}
};
XFillArcs(
display,
window,
gc,
filledArcs,
3
);XFillArcsOf the currentdisplayof the currentwindowusing thedrawing contextofgcdraw the following3arcs each referenced byx, y, width, height, starting angle, angle extentThus each arc is described by 6 points of data. This command queues untilxflush(display)is called.
XDrawString
XDrawString(
display,
window,
gc,
50, 600, // x and baseline y
text,
std::strlen(text)
);XDrawStringOf the currentdisplayof the currentwindowusing thedrawing contextofgcat x,y point50,600write the followingtextthat has a length ofstd::strlen(text). This command queues untilxflush(display)is called.
XDrawImageString
XDrawImageString(
display,
window,
gc,
250, 600,
text,
std::strlen(text)
);XDrawImageStringOf the currentdisplayof the currentwindowusing thedrawing contextofgcat x,y point250,600write the followingtextthat has a length ofstd::strlen(text)and give it a rectangular background. This command queues untilxflush(display)is called.
XDrawString16
XChar2b text16[] = {
{0, 'H'},
{0, 'i'},
{0, '!'}
};
// Draw 16-bit text
XDrawString16(
display,
window,
gc,
400, 600,
text16,
3
);
XDrawString16Of the currentdisplayof the currentwindowusing thedrawing contextofgcat x,y point50,600write the following 16-bittextthat has a length of3. This command queues untilxflush(display)is called.
XDrawText
XTextItem textItems[] = {
{(char*)"Hello ", 6, 0, None},
{(char*)"X11", 3, 0, None}
};
XDrawText(
display,
window,
gc,
50, 580, // x and baseline y
textItems,
2 // number of text items
);
XDrawTextOf the currentdisplayof the currentwindowusing thedrawing contextofgcat x,y points50, 580write out the followingtextItemsof count2. This command queues untilxflush(display)is called.
Finishing the Frame.
Once you have buffered up all your XDraw commands you can send them for processing with
XFlush(display);Conclusion
This is a good primer. We can see in the documentation that the raw X-11 does not natively handle buttons etc. Below is a entire working X-11 with a Canvas class to to various updates etc:
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <vector>
//region Region Canvas
class Canvas
{
private:
Display* display = nullptr;
Window window = 0;
GC gc = nullptr;
XImage* image = nullptr;
int width = 0;
int height = 0;
std::vector<uint32_t> pixels;
public:
Canvas(Display* display, Window window, GC graphicsContext,int width, int height): display(display), window(window), gc(graphicsContext)
{
resize(width, height);
}
~Canvas() { destroyImage(); }
void resize(int newWidth, int newHeight)
{
width = std::max(1, newWidth);
height = std::max(1, newHeight);
pixels.resize(static_cast<size_t>(width) * height);
clear(0x202020);
destroyImage();
Visual* visual = DefaultVisual(display, DefaultScreen(display));
int depth = DefaultDepth(display, DefaultScreen(display));
/*
* XCreateImage calculates bytes_per_line for us when the
* data pointer is initially null.
*/
image = XCreateImage(display, visual, depth, ZPixmap, 0, nullptr, width, height, 32, 0);
if (!image)
{
std::exit(1);
}
image->data = static_cast<char*>(std::malloc(image->bytes_per_line * height));
if (!image->data)
{
std::exit(1);
}
}
int getWidth() const
{
return width;
}
int getHeight() const
{
return height;
}
// Direct access to the raw framebuffer.
uint32_t* data()
{
return pixels.data();
}
const uint32_t* data() const
{
return pixels.data();
}
void clear(uint32_t rgb)
{
std::fill(pixels.begin(), pixels.end(), rgb);
}
void setPixel(int x, int y, uint32_t rgb)
{
if (x < 0 || x >= width || y < 0 || y >= height)
{
return;
}
pixels[static_cast<size_t>(y) * width + x] = rgb;
}
uint32_t getPixel(int x, int y) const
{
if (x < 0 || x >= width || y < 0 || y >= height)
{
return 0;
}
return pixels[static_cast<size_t>(y) * width + x];
}
void fillRect(int x, int y, int w, int h, uint32_t rgb)
{
int x0 = std::max(0, x);
int y0 = std::max(0, y);
int x1 = std::min(width, x + w);
int y1 = std::min(height, y + h);
for (int py = y0; py < y1; ++py)
{
for (int px = x0; px < x1; ++px)
{
setPixel(px, py, rgb);
}
}
}
void drawGradient()
{
for (int y = 0; y < height; ++y)
{
for (int x = 0; x < width; ++x)
{
uint8_t red = static_cast<uint8_t>(255.0 * x / std::max(1, width - 1));
uint8_t green = static_cast<uint8_t>(255.0 * y / std::max(1, height - 1));
uint8_t blue = 100;
uint32_t color = (static_cast<uint32_t>(red) << 16) | (static_cast<uint32_t>(green) << 8) | blue;
setPixel(x, y, color);
}
}
}
void present()
{
/*
* Convert our RGB framebuffer into the format expected by X11.
*
* XPutPixel is intentionally used here because it handles the
* current X11 visual correctly. It is simple but not especially
* fast. Later, you could optimize this using XShm or OpenGL.
*/
for (int y = 0; y < height; ++y)
{
for (int x = 0; x < width; ++x)
{
uint32_t rgb = getPixel(x, y);
unsigned long x11Pixel = ((rgb >> 16) & 0xff) << 16 | ((rgb >> 8) & 0xff) << 8 | (rgb & 0xff);
XPutPixel(image, x, y, x11Pixel);
}
}
XPutImage(display,window,gc,image, 0, 0, 0, 0, width, height);
XFlush(display);
}
private:
void destroyImage()
{
if (image)
{
XDestroyImage(image);
image = nullptr;
}
}
};
//endregion
int main()
{
Display* display = XOpenDisplay(nullptr);
if (!display) {return 1; }
int screen = DefaultScreen(display);
XID Xid = RootWindow(display, screen);
Window window = XCreateSimpleWindow(display, Xid,100,100,800,600,1, BlackPixel(display, screen), WhitePixel(display, screen));
XStoreName(display, window, "My C++ Canvas Framework");
XSelectInput(display, window, ExposureMask | KeyPressMask | StructureNotifyMask );
Atom deleteWindowMessage = XInternAtom(display, "WM_DELETE_WINDOW", False);
XSetWMProtocols(display,window,&deleteWindowMessage,1);
XMapWindow(display, window);
GC gc = DefaultGC(display, screen);
Canvas canvas(display, window, gc, 800, 600);
bool running = true;
while (running)
{
XEvent event;
XNextEvent(display, &event);
switch (event.type)
{
case Expose:
{
canvas.present();
break;
}
case ConfigureNotify:
{
int newWidth = event.xconfigure.width;
int newHeight = event.xconfigure.height;
if (newWidth != canvas.getWidth() || newHeight != canvas.getHeight())
{
canvas.resize(newWidth, newHeight);
// Redraw after resizing.
canvas.drawGradient();
canvas.fillRect(100,100,250,150,0xff0000);
canvas.present();
}
break;
}
case KeyPress:
{
KeySym key = XLookupKeysym(&event.xkey, 0);
if (key == XK_Escape) { running = false; }
break;
}
case ClientMessage:
{
if (static_cast<Atom>(event.xclient.data.l[0]) == deleteWindowMessage)
{
running = false;
}
break;
}
}
}
XCloseDisplay(display);
return 0;
}