Qt. Creating a console widget for a graphical application

Hello to good people.
When reading this heading, readers may think: why mix console and graphical applications - the console is not needed in the GUI application. But no, I dare say. Sometimes, combining a functional console with a full set of commands and graphical displays for convenient navigation and viewing data can result in a powerful tool.
And I have an example.
Starting to use the Redis fast key-value data warehouse for my projects, I found that at the moment there is not a single sane desktop application for viewing, editing and administering Redis databases. There is only a console from developers, the Redis Admin UI web interface, which requires .NET for its work (which in itself is already scaring away) and a couple of Ruby applications made, it seems, in haste, on the knee.
I would like to have something convenient and fast, like the Redis database itself. Therefore, I decided to fill this gap and write such a tool. Since you need fast - then C ++, since you need cross-platform - then Qt.

Redisconsole

Due to the fact that you cannot implement all the database features, and they may appear new every day, you had to add a console to the graphical interface. Based on which widget in Qt to simulate it, and how, and I want to tell you.


From lawlessness to total control



For the base console widget, I selected QPlainTextEdit. Firstly, it includes advanced text editing capabilities that we may need, and secondly, it allows you to add formatting: highlighting different elements with color would not hurt us.

So, create a descendant of QPlainTextEdit.

class Console : public QPlainTextEdit{};


Despite the fact that QPlainTextEdit is a simplified version of QTextEdit, it allows the user to do too many actions that are not permissible for a decent console.

Therefore, the first thing we will do is to limit everything that is possible. We pass from complete lawlessness to total control.

To do this, redefine the built-in slots that receive keystrokes and mouse clicks:

void Console::keyPressEvent(QKeyEvent *){}
void Console::mousePressEvent(QMouseEvent *){}
void Console::mouseDoubleClickEvent(QMouseEvent *){}
void Console::contextMenuEvent(QContextMenuEvent *){}


After these lines, the user can neither enter a character in the widget field, nor select a piece of text, or delete a line - complete blocking.

Stage of liberalization



Now let's move from a total ban to a reasonable democracy, while simultaneously resolving everything that is needed.

The first thing to do is define the prompt line:

// class definition
QString prompt;
// contructor
prompt = "redis> ";


And print the prompt line to the console:

// constructor
insertPrompt(false);
// source
void Console::insertPrompt(bool insertNewBlock)
{
    if(insertNewBlock)
        textCursor().insertBlock();
    textCursor().insertText(prompt);
}


It is necessary that when clicking with the mouse it was impossible to rearrange the cursor, but it was possible to make the console active:

void Console::mousePressEvent(QMouseEvent *)
{
    setFocus();
}


When entering ordinary letters, numbers and other useful characters, they should be added to the command line:

void Console::keyPressEvent(QKeyEvent *event)
{
    // …
    if(event->key() >= 0x20 && event->key() <= 0x7e
       && (event->modifiers() == Qt::NoModifier || event->modifiers() == Qt::ShiftModifier))
        QPlainTextEdit::keyPressEvent(event);
    // …
}


Symbols can be erased with the Backspace key, but not all, but only up to a certain point - so, God forbid, the invitation line is not lost:

void Console::keyPressEvent(QKeyEvent *event)
{
    // …
    if(event->key() == Qt::Key_Backspace
       && event->modifiers() == Qt::NoModifier
       && textCursor().positionInBlock() > prompt.length())
        QPlainTextEdit::keyPressEvent(event);
    // …
}


Determine the response of the widget to command input (when you press Enter):

void Console::keyPressEvent(QKeyEvent *event)
{
    // …
    if(event->key() == Qt::Key_Return && event->modifiers() == Qt::NoModifier)
        onEnter();
    // …
}


When entering the command, we cut a piece of text from the prompt line to the end of the text block and emit a signal to which you can attach the slot:

void Console::onEnter()
{
    if(textCursor().positionInBlock() == prompt.length())
    {
        insertPrompt();
        return;
    }
    QString cmd = textCursor().block().text().mid(prompt.length());
    emit onCommand(cmd);
}


Also, while the application is processing the command, set the checkbox to block the text field.

void Console::onEnter()
{
    // …
    isLocked = true;
}
void Console::keyPressEvent(QKeyEvent *event)
{
    if(isLocked)
        return;
    // …
}


Application - the widget’s parent will process the command and pass the result of execution to the console, thereby unlocking it:

void Console::output(QString s)
{
    textCursor().insertBlock();
    textCursor().insertText(s);
    insertPrompt();
    isLocked = false;
}


Team History



I would like the history of all entered commands to be saved and when you press the up / down keys it would be possible to navigate through it:

// class definition
QStringList *history;
int historyPos;
// source
void Console::keyPressEvent(QKeyEvent *event)
{
    // …
    if(event->key() == Qt::Key_Up && event->modifiers() == Qt::NoModifier)
        historyBack();
    if(event->key() == Qt::Key_Down && event->modifiers() == Qt::NoModifier)
        historyForward();
}
void Console::onEnter()
{
    // …
    historyAdd(cmd);
    // …
}
void Console::historyAdd(QString cmd)
{
    history->append(cmd);
    historyPos = history->length();
}
void Console::historyBack()
{
    if(!historyPos)
        return;
    QTextCursor cursor = textCursor();
    cursor.movePosition(QTextCursor::StartOfBlock);
    cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
    cursor.removeSelectedText();
    cursor.insertText(prompt + history->at(historyPos-1));
    setTextCursor(cursor);
    historyPos--;
}
void Console::historyForward()
{
    if(historyPos == history->length())
        return;
    QTextCursor cursor = textCursor();
    cursor.movePosition(QTextCursor::StartOfBlock);
    cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
    cursor.removeSelectedText();
    if(historyPos == history->length() - 1)
        cursor.insertText(prompt);
    else
        cursor.insertText(prompt + history->at(historyPos + 1));
    setTextCursor(cursor);
    historyPos++;
}


Making beautiful: console coloring



To do this, in the widget constructor, define the general color gamut for the console - the background is black, the letters of the command being entered are green:

QPalette p = palette();
p.setColor(QPalette::Base, Qt::black);
p.setColor(QPalette::Text, Qt::green);
setPalette(p);


When the prompt string is displayed, we make the font in green:

void Console::insertPrompt(bool insertNewBlock)
{
    // …
    QTextCharFormat format;
    format.setForeground(Qt::green);
    textCursor().setBlockCharFormat(format);
    // …
}


And when outputting the result of the command, we make a white font:

void Console::output(QString s)
{
    // …
    QTextCharFormat format;
    format.setForeground(Qt::white);
    textCursor().setBlockCharFormat(format);
    // …
}


All down!



I would also like that when the user enters a command, the scrollbar of the console text field is scrolled to the very bottom:

void Console::insertPrompt(bool insertNewBlock)
{
    // …
    scrollDown();
}
void Console::scrollDown()
{
    QScrollBar *vbar = verticalScrollBar();
    vbar->setValue(vbar->maximum());
}


Result



The result is a fun, beautiful and comfortable console. It took me just 120 lines of code. Of course, there are many more things that could be done, but the basic functionality is implemented.

References



The source code of the RedisConsole project on GitHub: https://github.com/ptrofimov/RedisConsole

There you can see the Console widget class and download the compiled application binary for Windows by clicking the “Downloads” button.

thanks


Also popular now: