Skip to content
Gennady Mohov edited this page Nov 28, 2021 · 1 revision

QtE56 Quick Start Guide.

The scheme below is described in Windows terms. It should be understood that for Linux the file names will be libXXXX.so - you should also take into account the bit depth of 32 or 64 bit implementation. If you have Qt 64 bit, then the libraries (so/dll files) must be 64 bit.

Example:

QtE56core32.dll --- 32-bit Windows implementation
libQtE56core64.so --- 64 Linux implementation

Architecture:
QtXX (*.dll) –--> Set of dll/so files where XX is the bit size
     |
     +----- |qte56coreXX.dll    |---> basic primitives
     +----- |qte56widgetsXX.dll |---> widgets
              |   |
              |   + QtE56.d ---> Bindings on the very D
              |        |
              |        + app.d ---> Application on D
              |
              + QtE56.cpp ---> Bindings on C++
                   |
                   + app.cpp ---> Application on C++

Note, that Qt can be represented (compiled) by different compilers, such as MINGW or MSVC. To keep implementation simple, QtE56 uses only MINGW on Windows or gcc on Linux. Compiling to msvc is possible, but has not been tested in detail since it is not used by me.

Build QtE56:

The most important thing is to build the files qte56coreXX.dll/so and qte56windowsXX.dll/so (where XX is 32/64). To do this, go to the build/qte56 folder. In this folder we have the source files *.pro (Qt), *.cpp, *.h - necessary to build QtE56 (qte56coreXX.dll/so, qte56widgetsXX.dll/so). Now using the C++ compiler included with your Qt you can build the files you need.

The easiest option for Linux:
$ cd build/qte56
$ qmake-qt5 qte56core.pro
$ make

The output will be qte56coreXX.so

In a similar way you can build files for Windows. You can also use QtCreator to build these files, the main thing is to create qte56coreXX.dll/so and qte56windowsXX.dll/so (where XX is 32/64).

One of the problems encountered when building these files is the wide variety of Qt versions and hence the different set of methods and properties in the classes. How to solve this problem, I do not know. For now I'm focusing on Qt6 and if I get errors when building with another Qt version, I just comment out the lines of code with this error. Disabling the error line like this causes one of qte56.d's method calls to stop working. So far, I haven't figured out a way around crashing the application when trying to call such a disabled method function.

Path to Qt libraries.

Now that we have the three files in one folder (qte56coreXX.dll/so + qte56windowsXX.dll/so + qte56.d) we can start creating applications using D and Qt. But, another problem arises. Our created application will ask qte56.d to load qte56coreXX.dll/so and qte56windowsXX.dll/so into memory for us but they themselves try to load other Qt*.dll/so and if it is not possible, we get an error message. To ensure successful loading you need to put paths to Qt*.dll/so files in the PATH environment variable, if you have Qt already installed, or copy additional required Qt*.dll/so files into our directory with the three files. For Windows it is enough to copy a small number of Qt*.dll files to make our application work. Please note that on Linux you will probably need to set the LD_LIBRARY_PATH environment variable in order to specify additional folders for loading the libraries.

An example of a typical application build and run on Linux:

$ export LD_LIBRARY_PATH=`pwd`
$ dmd app.d qte56.d -release -m64
$ ./app
The first application.

Now that we have everything set up, we will create our first application. This will be a kind of health test for QtE56.

Create a test.d file:
-----------------------------------
import core.runtime,qte56;
int main(string[] args) {
    if(LoadQt(dll.QtE6Widgets, false)) return 1;
    QApplication app = new QApplication(&Runtime.cArgs.argc,
        Runtime.cArgs.argv, 1);
    QLabel lb = new QLabel(null);
    lb.setText("Hello from D").show();
    return app.exec();
}
-----------------------------------
Compile and run our application
-----------------------------------
$ export LD_LIBRARY_PATH=`pwd`
$ dmd test.d qte56.d -release -m64
$ ./test

If our application window appears on the screen, then we were able to create and execute the D + Qt application.

Slots and Signals.

Before we look at the slot and signal model in QtE56 I want to point out the following things:

  1. QtE56 is a very small subset of the huge Qt framework.
  2. When implementing QtE56 I tried to keep Qt's class structure, class names, methods and properties and the general working model of Qt as close as possible.
  3. All the standard signals and slots of the classes implemented in QtE56 are available.
  4. Understanding how Qt works is necessary to understand QtE56.
  5. In most cases Qt's standard documentation is fully applicable to QtE56 class descriptions.

Let's look at the fundamental differences in the implementation of the QtE56 slot and signal model compared to Qt.

The first and very important difference. The standard Qt model expects the slot to be created at the C++ source compilation stage. The slot itself is a METHOD of a class. As many methods (slots) are added to the class description, that's how many we get. QtE56 uses a different approach, namely using a modified version of the QAction class as a repository for BOTH (set) slots. There are 20% different slots which cover 80% of the options we need.

List of available slots in QtE56.
  1. "Slot_AN()" -- void call(AdrClass, Nnumber);
  2. "Slot_ANI(int)" –- void call(AdrClass, Nnumber, int);
  3. "Slot_ANII(int, int)" -- void call(AdrClass, Nnumber, int, int);
  4. "Slot_ANIII(int, int, int)" -- void call(AdrClass, Nnumber, int, int, int);
  5. "Slot_ANB(bool)" –- void call(AdrClass, Nnumber, bool);
  6. "Slot_ANQ(QObject*)" -- void call(AdrClass, Nnumber, Qbbject*);

It is always possible to add new variants to C++ if you want to, but the need for this is quite rare. One of the hard parts is that Qt checks the number and types of the signal and slot arguments. Because of this, we have to adjust for Qt's restrictions by increasing the number of different slot variants.

Ok. By creating a ``QAction action = new QAaction(...);`` we obtain an action that actually stores our slot set. By doing so we open up the possibility to create slot signal links similar to:

conect(button, "clicked()", action, "Slot_AN()"); 
conect(button, "press(int)", action, "Slot_ANI(int)"); 

Now we have an instance of the Qaction class, it has the slot we want, and we can associate the signal with our slot.

Let's look at the following example:

Create a test1.d file
-----------------------------------
import core.runtime,  qte56;
extern (C) {  // (1)
    // (2)
    void onAction(void* uk, int n)  { 
        runAction(); 
    } 
}
// (3)
void runAction() { // this is slot
    msgbox("applicationDisplayNameChanged() ...");
}
QAction acAction;
int main(string[] args) {
    if(LoadQt(dll.QtE6Widgets, false)) return 1;
    QApplication app = new QApplication(&Runtime.cArgs.argc,
        Runtime.cArgs.argv, 1);
    QLabel lb = new QLabel(null);
    lb.resize(640, 480);
    lb.setText("Hello from D").show();
    // Create action with my slot
    // (4)
    acAction = new QAction(null, &onAction,  &app, 2);
    // Connect standart signal with my slot in QAction
    // (5)
    acAction.connects(app, "applicationDisplayNameChanged()", acAction, "Slot_AN()");
    // Generate standart signal
    // (6)
    app.setApplicationDisplayName!string("newNameApp");
    return app.exec();
}
-----------------------------------
Compile and run our application
-----------------------------------
$ dmd test1.d qte56.d -release -m64
$ ./test1

In this example, let's see how to link a standard Qt signal to our QAction-based slot. Label four (label 4) - we create a new QAction. The constructor accepts four parameters:

  1. Parent. Usually null or this. Here null, since there is no parent.
  2. The C address of the function that will be called when the slot is triggered (signal is caught). In this example onAction().
  3. The address of the class instance in which the slot processing method is located. In this example, this is any address, since it not used in this example.
  4. int value (number). This number is stored in each particular QAction instance and will be retrieved by the call function, in our case onAction()

If we look at the list of slots (List of available slots in QtE56.) We see at number 1 how our slot with C (label 2) code will be called. The specific call description is the onAction(...) call with the right parameters.

The connects function (label 5) allows you to associate a signal to a slot. This is a standard Qt function. This function has four parameters:

  1. Object, signal emitter
  2. Symbolic description of the signal
  3. Object, the owner of the slot
  4. Symbolic description of the slot

In our case, we associate app: "applicationDisplayNameChanged()" with acAction: "Slot_AN()". Now, when the "applicationDisplayNameChanged()" signal is emitted, a specific acAction: "Slot_AN()" slot will be activated. The activation of this slot will call the onAction(...) function (mark 2) which in turn will call the function runAction(...) (mark 3). Now, we are ready to activate the signal itself. This is done by a line (label 6) which changes the name of the application and as consequently a signal is generated which we catch by our slot.

Let's look at the most standard layout of the source code for dealing with slots and signals. It will be a simple application with one class that will contain the visual elements of Qt. The same class will contain the methods (slots) for processing the signals.

Create a test2.d file
-----------------------------------
import core.runtime,  qte56;
extern (C) {
    // (1)
    void onAction(CMyWidget* uk, int n) { 
        (*uk).runAction(); 
    } 
}
class CMyWidget : QWidget {
    QLabel lb;
    QPushButton kn;
    QAction acAction;
    this(QWidget parent) {
        super(null); resize(300, 130);
        lb = new QLabel(this);
        lb.resize(300, 50).setStyleSheet("background: pink");
        kn = new QPushButton("Press Me", this);
        kn.move(110, 60).resize(80, 30);
        // (2)
        acAction = new QAction(this, &onAction, aThis);
        // (3)
        connects(kn, "clicked()", acAction, "Slot_AN()");
    }
    // (4)
    void runAction() {
        lb.setText("A button was pressed ...");
    }
}
int main(string[] args) {
    if(LoadQt(dll.QtE6Widgets, false)) return 1;
    QApplication app = new QApplication(&Runtime.cArgs.argc,
        Runtime.cArgs.argv, 1);
    // (5)
    CMyWidget mw = new CMyWidget(null);
    // (6)
    mw.saveThis(&mw);
    mw.show();
    return app.exec();
}
-----------------------------------
Compile and run our application
-----------------------------------
$ export LD_LIBRARY_PATH=`pwd`
$ dmd test2.d qte56.d -release -m64
$ ./test2

Let's take a closer look at this example. The main difference is that there is a new class CMyWidget inherited from QWidget. Its constructor is simple and the same as the previous example. Let's look at the creation of a QAction (label 2). Three parameters are defined (not four), that's because the fourth parameter has a default value of zero. The first parameter is the parent (see the Qt documentation), the second parameter is the C address of the function (the broker), and the third parameter is the address of an instance of the class. It is this address that allows control to be passed from the C function to the slot (method) of the D class (label 1).

Note the line (label 5) to create an instance of the CMyWidget class. The parent is null, there is no this, nothing to refer to. A very IMPORTANT line (label 6), it remembers the address of an instance of the class, which can then be read by the aThis property (label 2).

One may wonder why an additional broker (C function) is used. The answer is that this is because we want to use QtE56 not only with the D programming language, but also with other programming languages. I tried using QtE56 with `forth` and `tcl`. Everything works just fine.

Let's look at another example. Here we are going to intercept the signal containing QString.

Create a test3.d file
-----------------------------------
import std.stdio, std.conv;
import qte56, core.runtime;
extern (C) {
    void onTest(CTest* uk, int n)       { (*uk).runTest();    }
    void onChTitle(CTest* uk, int n, void* qs) { (*uk).runChTitle(qs); }
}
class CTest: QWidget {
    QPushButton kn1;
    QAction acTest, acChTitle;
    int nn;
    this() {
        super(null);
        // Qt docomentation
        resize(200, 50); setStyleSheet("background: Moccasin");
        kn1 = new QPushButton("Press Me", this);
        // (1)
        acTest = new QAction(this, &onTest,  aThis);
        connects(kn1, "clicked()", acTest, "Slot_AN()");
        // (2)
        acChTitle = new QAction(this, &onChTitle, aThis);
        connects(this, "windowTitleChanged(QString)", acChTitle, "Slot_ANQS(QString)");
    }
    void runTest() { // slot ... (3)
        setWindowTitle("Hello " ~ to!string(nn++));
    }
    void runChTitle(void* qs) { // slot ... (4)
        QString qss = new QString('+', qs); // (5)
        msgbox("cath signal: " ~ qss.String, "run slot ...");
    }
}
int main(string[] args) {
    if (1 == LoadQt(dll.QtE6Widgets, false)) return 1;
    QApplication app = new QApplication(&Runtime.cArgs.argc, Runtime.cArgs.argv, 1);
    CTest wn = new CTest(); wn.saveThis(&wn).show; // (6)
    return app.exec();
}
-----------------------------------
Compile and run our application
-----------------------------------
$ export LD_LIBRARY_PATH=`pwd`
$ dmd test3.d qte56.d -release -m64
$ ./test3

This example is similar to the previous one. The only difference is that we will intercept the signal having QString as argument. The string (label 1) defines a QAction and connects, which help create the slot of button operation processing (label 3). The purpose of this slot is to change the title of the window, thus causing Qt generate the "windowTitleChanged(QString)" signal. The lines (label 2) define the slot (via the &onChTitle -- void onChTitle(...) -- *.runChTitle(qs) -- void runChTitle(void* qs)) (mark 4) which will handle received signal of title change.

 <p>
 The line (label 5) is very important here allowing you to create an instance of the QtE56 class (QString) from
 pointer to a QString pointer that comes from Qt. The first argument in the '+' constructor says that
 that we are catching and saving a QString pointer that comes from Qt into qss. The msgbox(...) function simply displays
 the resulting string using qss.String. 
Events QtE56 and Qt.

There are a lot of events in the Qt framework. QtE56 has a limited number of event handlers. You can find implemented events by browsing the qte56.d file. Events are handled using classes. An example is QResizeEvent. An approximate list can be obtained by running:

$ grep -E '\bQ(.*)vent' qte56.d

Examples of event class names: QResizeEvent, QWheelEvent, QMouseEvent, etc. Now that we have the event classes, we can set event capture with the following methods: setKeyPressEvent(...), setResizeEvent(...), etc. You can get an approximate list:

$ grep -E '\bset(.*)vent' qte56.d
The parameters of these methods (event capture setters) have two parameters. They are adr (address of the C function broker) and AdrThis (address of the class instance containing the event handler).

Let's look at a bit of code as an example of how to use the handler setup:

...
extern (C) {
    // (2)
    void onPressKeyEvent(CMgw* uk, void* qs) { (*uk).runPressKeyEvent(qs); }
}
class CMyWidget: QWSidget {
    ...
    QEdit teEdit;
    ...
    teEdit = new QEdit(this);
    teEdit.setKeyPressEvent(&onPressKeyEvent, aThis ); // (1)
    ...
    void runPressKeyEvent(void*  qs) { // (3)
        QKeyEvent qe = new QKeyEvent('+', ev); 
        switch(qe.key) {
            case '"': insParaSkobki("\"");  break;
            case '(': insParaSkobki(")");   break;
            case '[': insParaSkobki("]");   break;
            case '{': insParaSkobki("}");   break;
            case QtE.Key.Key_Return:
                    strBeforeEnter = tb.text!string();
                break;
            case QtE.Key.Key_L:
                if(qe.modifiers == QtE.KeyboardModifier.ControlModifier) {
                    editSost = Sost.Cmd;
                }
                break;
            default: break;
    }
}
...
CMyWidget w = new CMyWidget(null);
w.saveThis(&w);
...

If you examine this source code more closely, you can see many common elements with the previous examples. There is a broker assignment (C function) (label 1), as well as a call to the slot itself (label 2). In the parameters there is *void that there is a pointer to an instance of one of the event classes. And the slot itself, (label 3), where typing (creating the right event in QtE56 terms) takes place. This is actually QKeyEvent qe = new QKeyEvent('+', ev);

QtE56 and QDesigner of Qt.

Qdesigner generates two kinds of output files:

  1. File *.ui (xml file describing the form and the elements in it).
  2. File *.qrc (a Qt resource file). This file can be parsed by the Qt resource compiler and produces a binary file suitable for embedding in D source code.

There is a special QFormBuilder class to handle *.ui files. The `load(...)` method loads the ui file from the D string. The output is a QtE5 QWidget that can be embedded as a main widget in a form. The standard application is as follows:

class QtE56Help: QMainWindow {
    ...
    QFormBuilder  qfb;
    QAction acAboutQt, knNabor ...
    QPushButton knNabor;
    ...
    this() {
        super(null);
        qfb = new QFormBuilder(this); // (1)
        // set QWidget from QDesigner on main widget form
        setQtObj((qfb.load(":/fQtE56help.ui")).QtObj); // (2)
        // Find address elements and save its in class of QtE56.
		// (3)
        acAboutQt  = new QAction('+', qfb.findChildAdr!string("actionAboutQt"),  this, &onAbout,  aThis, 2); 
        acAboutApp = new QAction('+', qfb.findChildAdr!string("actionAboutApp"), this, &onAbout,  aThis, 1); 
        knNabor = new QPushButton('+', qfb.findChildAdr!string("pushButton_Nabor")); 
        // and etc ...
        
        ...
    }
    ...
}

In this example, you can see the main points:

  1. (label 1) creating a QFormBuilder instance.
  2. (label 2) Loading the ui file and anchoring the resulting widget as a main form widget.
  3. (label 3) Finding and linking form elements and QtE56 classes.

From the example above you can see how to process *.ui files. You can also use qrc files in the same way. To do this, you need to get their binary form by compiling them. An example of how the Qt resource compiler compiles source files into output binaries. Such a *.qrc resource file can be obtained from QDesigner (see the Qt documentation). Compiling a3.qrc from QDesigner:

$ rcc -binary a3.qrc -o a3.rcc

After this preprocessing we get a binary file. Such a file may contain, among other things, text from a ui file. We also need to define a class for working with resources (QResource) and use it to register the resources in our application. Note that the resource is loaded from a byte array which was earlier imported by means of D.

Example:
ubyte* gResource = cast(ubyte*)import("a3.rcc");
...
QResource qrs = new QResource(); qrs.registerResource(gResource);

Now to access the elements setQtObj((qfb.load(":/fQtE56help.ui")).QtObj;) For an understanding of the `:` in the name line - see Qt's resource documentation.

Conclusion.

This is a short introduction to how QtE56 works. I have used this library in 32/64 projects on Windows, Linux and MacOs. With it I have used Qt from the following programming languages: D, C++, Forth, tclsh.

Acknowledgements.

I want to express my great thanks to the creator of the excellent D programming language Walter Bright. Since I started to learn C++ programming with his Zortech C++ it is twice as pleasant for me to write programs every day in the D programming language.

Clone this wiki locally