Pull to refresh

Qt. Создание виджета-консоли для графического приложения

Reading time 5 min
Views 24K
Привет добрым людям.
При прочтении этого заголовка читатели могут подумать: зачем смешивать консольные и графические приложения – консоль в GUI-приложении не нужна. А вот и нет, смею заметить. Иногда совмещение функциональной консоли с полным набором команд и графического отображения для удобной навигации и просмотра данных может дать в итоге мощный инструмент.
И у меня есть пример.
Начав использовать быстрое key-value хранилище данных Redis для своих проектов, я обнаружил, что на данный момент нет ни одного вменяемого desktop-приложения для просмотра, редактирования и администрирования баз данных Redis. Есть только консоль от разработчиков, веб-интерфейс Redis Admin UI, который для своей работы требует .NET (что само по себе уже отпугивает) и пару Ruby-приложений, сделанных, похоже, на скорую руку, на коленке.
Хотелось бы иметь что-то удобное и быстрое, как сама база данных Redis. Поэтому я решил восполнить этот пробел и написать такой инструмент. Так как нужен быстрый – то C++, так как нужен кроссплатформенный – то Qt.

RedisConsole

Из-за того, что все возможности базы данных не реализуешь, да они и могут появляться каждый день новые, нужно было добавить в графический интерфейс консоль. На основе какого виджета в Qt ее имитировать, и как, и хочу вам рассказать.


От беспредела к тотальному контролю



Для базового виджета консоли я выбрал QPlainTextEdit. Во-первых, он включает в себя расширенные возможности редактирования текста, которые нам могут понадобиться, а во-вторых, он позволяет добавлять форматирование: подсветка разных элементов цветом нам бы не помешала.

Итак, создаем потомка QPlainTextEdit.

class Console : public QPlainTextEdit{};


Несмотря на то, что QPlainTextEdit – это упрощенная версия QTextEdit, он разрешает пользователю делать черезчур большое количество действий, непозволительное для приличной консоли.

Поэтому первое, что мы сделаем, — это ограничим все, что только можно. Перейдем от полного беспредела к тотальному контролю.

Для этого переопределим встроенные слоты, получающие нажатия клавиш и клики мышки:

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


После этих строк пользователь не сможет ни ввести символ в поле виджета, ни выделить кусок текста, ни удалить строку – полная блокировка.

Этап либерализации



Теперь пойдем от тотального запрета к разумной демократии, попутно разрешая все, что понадобится.

Первое, что сделаем – это определим строку приглашения (prompt):

// class definition
QString prompt;

// contructor
prompt = "redis> ";


И выведем строку приглашения в консоль:

// constructor
insertPrompt(false);

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


Нужно, чтобы при клике мышкой нельзя было переставить курсор, но можно было сделать консоль активной:

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


При вводе обычных букв, цифр и других полезных символов, они должны добавляться в строку команды:

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


Символы можно стирать клавишей Backspace, но не все, а только до определенного момента – чтобы строка приглашения не дай бог не затерлась:

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


Определим реакцию виджета на ввод команды (при нажатии клавиши Enter):

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


При вводе команды мы вырезаем кусок текста от строки приглашения до конца текстового блока и испускаем сигнал, к которому можно будет присоединить слот:

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


Так же на время обработки команды приложением, устанавливаем флажок блокировки текстового поля.

void Console::onEnter()
{
    // …
    isLocked = true;
}


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


Приложение – родитель виджета обработает команду и передаст консоли результат выполнения, тем самым разблокируя ее:

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


История команд



Хотелось бы, чтобы история всех вводимых команд сохранялась и при нажатии клавиш вверх/вниз можно было бы по ней перемещаться:

// 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++;
}


Делаем красиво: раскраска консоли



Для этого в конструкторе виджета определим общую гамму цвета для консоли – фон черный, буквы вводимой команды – зеленые:

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


При выводе строки приглашения делаем шрифт зеленого цвета:

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


А при выводе результата выполнения команды делаем шрифт белого цвета:

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


Все вниз!



Также хотелось бы, чтобы когда пользователь вводит команду, скроллбар текстового поля консоли проматывался до самого низа:

void Console::insertPrompt(bool insertNewBlock)
{
    // …
    scrollDown();
}

void Console::scrollDown()
{
    QScrollBar *vbar = verticalScrollBar();
    vbar->setValue(vbar->maximum());
}


Результат



В результате получилась веселая, красивая и удобная консолька. У меня это заняло всего 120 строк кода. Конечно, есть еще много вещей, которые можно было бы сделать, но основная функциональность реализована.

Ссылки



Исходный код проекта RedisConsole на GitHub: https://github.com/ptrofimov/RedisConsole

Там можно посмотреть класс виджета Console и скачать скомпилированный бинарник приложения для Windows, нажав кнопку «Downloads».

Спасибо

Tags:
Hubs:
+38
Comments 21
Comments Comments 21

Articles