Редактор на Go и QML

17 minute read

Вольный пересказ документации к QML, адаптированный для языка программирования golang.

Все манипуляции производились на убунте. Не обещаю, что это взлетит на других дистрах. И вообще не факт, что будет работать на винде. Но, чем черт не шутит?

QML - хорошая вещь для написания десктопных интерфейсов. Сейчас активно пилится пакет go-qml. Пока что, этот пакет еще не достаточно стабилен, но уже многое умеет.

Надо заметить, что в этом примере не будет использоваться мощный инструмент RegisterType, который позволяет использовать свои типы в QML

Презентация возможностей пакета в коротком видео.

Установка Qt

Для моей убунты 12.04 сперва нужно установить зависимости.

$ sudo add-apt-repository ppa:ubuntu-sdk-team/ppa
$ sudo apt-get update
$ sudo apt-get install ubuntu-sdk qtbase5-private-dev qtdeclarative5-private-dev libqt5opengl5-dev

Правда, есть одно но. Стандартная установка затащит Qt версии 5.0.1, а в ней нет, например, QtQuick.Controls. Поэтому немного поизвращаемся с установкой свежей версии.

Самый удобный вариант установить последнюю версию Qt - это воспользоваться .run файлом с оф. сайта. Место для установки можно выбрать любое. После установки, чтобы приложение с go-qml могли нормально собираться, нужно указать где находятся исходники нашей версии библиотеки:

$ export LD_LIBRARY_PATH=/home/artem/Qt5.2.1/5.2.1/gcc/lib:$LD_LIBRARY_PATH

Теперь устанавливаем сам пакет go-qml:

$ go get gopkg.in/qml.v0

Простая программа

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

package main

import (
    "fmt"
    "gopkg.in/qml.v0"
    "os"
)

func main() {
    if err := run(); err != nil {
        fmt.Fprintf(os.Stderr, "error: %v\n", err)
        os.Exit(1)
    }
}

func run() error {
    qml.Init(nil)

    engine := qml.NewEngine()
    component, err := engine.LoadFile("Example.qml")
    if err != nil {
        return err
    }

    win := component.CreateWindow(nil)

    win.Show()
    win.Wait()

    return nil
}

Любая Go программа, работающая с QML, должна выполнять несколько шагов:

  • Инициализировать пакет qml (qml.Init(nil))
  • Создать движок для загрузки и выполнения QML контента (engine := qml.NewEngine())
  • Сделать значения и типы из Go доступными для QML (Context.SetVar и RegisterType)
  • Загружать QML контент (component, err := engine.LoadFile("Main.qml"))
  • Создавать новое окно с контентом (win := component.CreateWindow(nil))
  • Показывать созданное окно и ждать его закрытия (win.Show(), win.Wait())

И очень простой QML код из файла Example.qml, который создает регион с текстом:

import QtQuick 2.0

Rectangle {
    width: 360
    height: 360
    color: "grey"

    Text {
        id: windowText
        anchors.centerIn: parent
        text: "Hello QML in Go!"
    }
}

В результате должно полуится серенькое окно с текстом:

img

Кнопки для редактора

Теперь можем приступать к созданию нашего редактора. Начнем с кнопочек открытия и сохранения файла. Как всегда, есть два способа. В первом случае, мы сделаем отдельный файл с названием Botton.qml и содержанием:

import QtQuick 2.0

Rectangle {
    id: button
    radius: 6
    border.width: 3
    border.color: "#ffffff"
    width: 150; height: 75
    property string label: ""
    color: "#eeeeee"
    signal buttonClick()
    onButtonClick: {
    }

    Text{
        id: buttonLabel
        anchors.centerIn: parent
        text: label
    }

    MouseArea {
        id: buttonMouseArea

        anchors.fill: parent
        onClicked: buttonClick()
    }
}

Это будет новый тип в QML который мы можем использовать в нашем главном файле. Не нужно писать ни каких импортов, так как Button.qml находится в той же папке, что и Example.qml

import QtQuick 2.0

Rectangle{
    width: 360
    height: 360
    color: "grey"

    Button{
        id: loadButton
        label: "Load"
        onButtonClick: {
            console.log("Hello!")
        }
    }
}

Вполне работоспособный пример, который выглядит примерно вот так: img

При клике на кнопку, в консоли будет писаться "Hello worl!".

Готовые компоненты

Такой подход дает полный контроль над внешним видом и поведением компонента, но требует больших временных затрат. Поэтому будем обходить гору и воспользуемся компонентом ToolButton из QtQuick.Controls:

import QtQuick 2.0
import QtQuick.Controls 1.0

Rectangle {
    width: 360
    height: 360
    color: "grey"

    ToolButton {
        id: loadButton
        x: 8
        y: 8
        text: "Load"
        clicked: {
            console.log("Load")
        }
    }

    ToolButton {
        id: saveButton
        x: 70
        y: 8
        text: {
            console.log("Save")
        }
    }
}

Теперь у нас есть две кнопочки:

img

Добавим немного жизни и радости к этому скучному дизайну. Создадим отдельный тип кнопок основанный на ToolButton и добавим к нему стилей QtQuick.Controls.Styles

import QtQuick 2.0
import QtQuick.Controls 1.0
import QtQuick.Controls.Styles 1.1

ToolButton {
    style: ButtonStyle {
        background: Rectangle {
            implicitWidth: 100
            implicitHeight: 25
            border.width: control.activeFocus ? 2 : 1
            border.color: "#888"
            radius: 4
            gradient: Gradient {
                GradientStop { position ; color: control.pressed ? "#ccc" : "#eee" }
                GradientStop { position: 1 ; color: control.pressed ? "#aaa" : "#ccc" }
            }
        }
    }
}

Используем этот тип в основном файле. А за одно, добавим многострочное текстовое поле для редактирования:

import QtQuick 2.0
import QtQuick.Controls 1.0

Rectangle {
    width: 360
    height: 360
    color: "grey"

    TextArea {
        id: textArea
        x: 8
        y: 74
        width: 344
        height: 278
    }

    ExampleButton {
        id: loadButton
        x: 8
        y: 8
        text: "Load"
        onClicked: {
            console.log("Load")
        }
    }

    ExampleButton {
        id: saveButton
        x: 140
        y: 8
        text: "Save"
        onClicked: {
            console.log("Save")
        }
    }
}

В итоге, получается готовый интерфейс нашего маленького приложения.

img

Реализуем логику

Добавляем файловый диалог и загрузку файлов для редактирования. Для этого используем компоненты из QtQuick.Dialogs

import QtQuick 2.0
import QtQuick.Controls 1.0
import QtQuick.Dialogs 1.0

Rectangle {
    ExampleButton {
        id: loadButton
        //...
        onClicked: {
            console.log("Load")
            fileDialogLoad.open()
        }
    }

    //...
    FileDialog {
        id: fileDialogLoad
        folder: "."
        title: "Choose a file to open"
        selectMultiple: false
        onAccepted: {
            console.log("Accepted: " + fileDialogLoad.fileUrl)
        }
    }
}

onAccepted - сработает тогда, когда в диалоговом окне будет выбран нужный файл.

Следующий шаг - самое интересное. Научимся передавать значения из QML в Go и наоборот. Для этого сделаем отдельный тип Editor:

type Editor struct {
    Text string
}

func (e *Editor) SelectFile(fileUrl string) {
    fmt.Println("Selected file: ", fileUrl)
    e.Text = fileUrl
    qml.Changed(e, &e.Text) // нужно, чтобы qml узнал о обновлении переменной
}

Как видно, метод SelectFile получает строку и записывает ее в параметр Text. Нужно привыкнуть пользоваться конструкцией qml.Changed(e, &e.Text) - именно этот вызов говорит нашему приложению что нужно обновить параметры в qml.

Пока не совсем понятно, зачем все это. Нужно передать этот тип в QML. Для этого есть методы SetVar, SetVars.

func run() error {
//...

context := engine.Context()
context.SetVar("editor", &Editor{})

//...
}

Так, все немного проясняется. Теперь нужно как-то захендлить Go переменную в qml коде. И тут нет ничего сложного:

TextArea {
    //...
    text: editor.text
}

ExampleButton {
    id: saveButton
    //...
    onClicked: {
        console.log("Save")
        editor.saveFile(textArea.text)
    }
}

FileDialog {
    //...
    onAccepted: {
        console.log("Accepted: " + fileDialogLoad.fileUrl)
        editor.selectFile(fileDialogLoad.fileUrl)
    }
}

Ага, теперь все понятно. editor.text - это обращение к параметру Editor.Text, a editor.selectFile(fileDialogLoad.fileUrl) - это вызов метода Editor.SelectFile(fileUrl string)

Последний штрих - это, собственно, работа с файлами. Загрузка контента и сохранение изменений:

type Editor struct {
    Text    string
    FileUrl string
}

func (e *Editor) SelectFile(fileUrl string) {
    fmt.Println("Selected file: ", fileUrl)
    e.FileUrl = strings.Replace(fileUrl, "file:/", "", 1)
    dat, err := ioutil.ReadFile(e.FileUrl)
    if err != nil {
        log.Println(err)
    }
    e.Text = string(dat)
 }

func (e *Editor) SaveFile(text string) {
    dat := []byte(text)
    err := ioutil.WriteFile(e.FileUrl, dat, 0644)
    if err != nil {
        log.Println(err)
    }
}

Вот и все. Наш маленький редактор, написанный с использованием Go и QML готов. Теперь можно браться за написание своей вижуал студии. Все исходники можно стянуть с гитхаба.