Редактор на Go и QML
Вольный пересказ документации к 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!"
}
}
В результате должно полуится серенькое окно с текстом:
Кнопки для редактора
Теперь можем приступать к созданию нашего редактора. Начнем с кнопочек открытия и сохранения файла. Как всегда, есть два способа. В первом случае, мы сделаем отдельный файл с названием 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!")
}
}
}
Вполне работоспособный пример, который выглядит примерно вот так:
При клике на кнопку, в консоли будет писаться "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")
}
}
}
Теперь у нас есть две кнопочки:
Добавим немного жизни и радости к этому скучному дизайну. Создадим отдельный тип кнопок основанный на 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")
}
}
}
В итоге, получается готовый интерфейс нашего маленького приложения.
Реализуем логику
Добавляем файловый диалог и загрузку файлов для редактирования. Для этого используем компоненты из 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 готов. Теперь можно браться за написание своей вижуал студии. Все исходники можно стянуть с гитхаба.