vak: (Default)
[personal profile] vak
Здесь будет пост с большим количеством скучного исходного кода. Я покажу базовый механизм, как строить оконные приложения (GUI) для мака. Кто не интересуется - смело пролистывайте.

Раньше "родные" маковские приложения положено было писать на языке Objective-C. Это такой гибрид ужа и ежа Си и Смолтока. У языка своя интересная история, как и у мака, но он был компромиссом со своими проблемами, и потихоньку сошёл на нет. Эппл придумал другой язык в замену, называется Swift. Он быстро развивается, и в последней версии достиг достаточной совместимости с Си++. Я ждал этого момента, чтобы попробовать поиграться со Swift.

Я ведь в GUI совсем не знаток. За всю жизнь не наваял ни одной приличной GUI-программы. Мои потребности в оконных интерфейсах ограничены примитивными случаями. Но механизмы знать полезно.

Обычно оконные интерфейсы принято "рисовать" графически. В смысле, строить из блоков в навороченных средах разработки типа Visual Studio или XCode. Долго и нудно возюкать мышкой и к каждому элементу настраивать сотни каких-то маловразумительных параметров. К счастью, для Swift появилась возможность всё делать прямо в исходном коде. Так называемый тулкит SwiftUI. Его я и покажу. Впрочем, он успешно уживается с визуальной средой XCode. То есть можно GUI и мышкой набрасывать при желании.

Swift хорошо уживается с Си++. Можно из Swift создавать объекты классов Си++ и обращаться к его полям и методам. И в обратную сторону: обращаться к классам и функциям Swift из кода Си++. Это крайне важная фича, и без неё Swift для меня малоинтересен. По простой причине: переносимость. Софт, которым я обычно занимаюсь, должен работать на всех платформах Linux (в том числе Андроид), мак (часто также iOS), Windows. По этой причине суть приложения (часто называют "бизнес-логика") пишется безотносительно к среде выполнения. Си++ обычный выбор. И потом эта бизнес-логика через хорошо определённые стыки привязывается к юзерскому интерфейсу конкретной платформы. Чтобы приложение смотрелось как "родное", лучше его делать на языке, родном для платформы. Для Windows это нынче C#, для Андроида Kotlin или Dart, для мака Swift. Скриптовые языки я тут обхожу стороной: от них больше проблем чем пользы.

Рассмотрим простейший пример приложения: целочисленный счётчик и пара кнопок "+" и "-", его изменяющих.


Всё приложение будет состоять из шести файлов. Исходники берите с Гитхаба.
  • MainApp.swift - главная процедура приложения (main).
  • ContentView.swift - главное окно приложения. Задаёт расположение графических элементов интерфейса в окне.
  • ModelProxy.swift - определяет реакцию на действия юзера. Когда кликаете на кнопочку - вызывается один из методов этого класса.
  • ModelCxx.cpp - реализация логики приложения. На самом деле класс ModelProxy сам ничего не делает, а вызывает методы Си++.
  • ModelCxx.h - определение интерфейса к логике приложения.
  • Package.swift - скрипт для сборки всего этого в кучу. Отдалённый аналог makefile, но на том же языке Swift.
Заметьте: это всё. Не нужны никакие make или cmake или ninja. Не нужна среда XCode с её скриптами. В состав компилятора входит утилита swift - нею и делается сборка. Файлы расположены по папкам следующим образом:
$ tree
.
├── Package.swift
└── Sources
├── Application
│ └── MainApp.swift
├── Gui
│ ├── ContentView.swift
│ └── ModelProxy.swift
└── ModelCxx
├── ModelCxx.cpp
└── ModelCxx.h

MainApp.swift

Определяем процедуру main(), с которой стартует приложение. Здесь вся суть в создании экземпляра класса ContentView, который реализует главное окно приложения. Остальное обёртка.
import SwiftUI
import Gui

@main
struct MainApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

ContentView.swift

Определяем класс, задающий в декларативном стиле компоновку главного окна приложения. Контейнер HStack располагает элементы интерфейса горизонтально. Ставим кнопочку "-" (Button), затем значение счётчика (Text), затем кнопочку "+". Заметьте, как для действий и значений вызываются методы и поля объекта proxy класса ModelProxy.
import SwiftUI

public struct ContentView: View {
@ObservedObject var proxy: ModelProxy = ModelProxy.shared

public var body: some View {
HStack {
Button("-") {
proxy.decrement()
}
Text("\(proxy.count)")
Button("+") {
proxy.increment()
}
}
.padding()
}

public init() {
proxy.setup()
}
}

#Preview {
ContentView()
}

ModelProxy.swift

Здесь мы определяем синглтон, куда приходят действия юзера, и откуда отображается нужная юзеру информация в главном окне. Но поскольку мы хотим всю бизнес-логику приложения иметь на Си++, здесь в классе ModelProxy мы просто пробрасываем все вызовы в объект model класса ModelCxx. И в обратную сторону: код из Си++ будет вызывать процедуру updateCount() для изменения отображаемого значения счётчика.
import Foundation
import ModelCxx

class ModelProxy: ObservableObject {
@Published var count: Int = 0

static let shared = ModelProxy()

// Allocate C++ object.
private var model = ModelCxx()

func setup() {
model.setup()
}

func increment() {
model.increment()
}

func decrement() {
model.decrement()
}
}

public func updateCount(val: Int) {
ModelProxy.shared.count = val
}

ModelCxx.h

Определяем интерфейс к бизнес-логике нашего приложения. Целочисленный счётчик и методы для инициализации, увеличения и уменьшения.
class ModelCxx {
private:
int count{ 123 };

public:
void setup();
void increment();
void decrement();
};

ModelCxx.cpp

Собственно бизнес-логика. Заметьте как вызывается метод Gui::updateCount() из Swift для обновления счётчика в окошке. Файл "Gui-Swift.h" автоматически создаётся компилятором Swift. В нём присутствуют все public классы и функции из нашего модуля Gui. В данном случае public func updateCount(val: Int).
#include "ModelCxx.h"
#include "Gui-Swift.h"

void ModelCxx::setup()
{
Gui::updateCount(count);
}

void ModelCxx::increment()
{
count++;
Gui::updateCount(count);
}

void ModelCxx::decrement()
{
count--;
Gui::updateCount(count);
}

Package.swift

Теперь опишем структуру нашего проекта для компилятора. Никаких специальных скриптов: используется тот же язык Swift. Инициализируем структуру специального класса Package. Исходники разбиты на три модуля: два свифтовых (Application и Gui) и один сиплюсплюсный (ModelCxx).
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
name: "CountCxx",
platforms: [
.macOS(.v14),
],
targets: [
.target(
name: "Gui",
swiftSettings: [.interoperabilityMode(.Cxx)]
),
.target(
name: "ModelCxx",
dependencies: ["Gui"],
publicHeadersPath: ".",
cxxSettings: [.unsafeFlags([
// Quick hack to find includes generated by Swift
"-I", ".build/arm64-apple-macosx/debug/Gui.build",
"-I", ".build/x86_64-apple-macosx/debug/Gui.build",
])]
),
.executableTarget(
name: "Application",
dependencies: ["ModelCxx", "Gui"],
swiftSettings: [.interoperabilityMode(.Cxx)]
),
],
cxxLanguageStandard: .cxx20
)
С этой версией компилятора Swift не всё оказалось гладко. По какой-то причине при компиляции кода Си++ не ставится путь к файлу Gui-Swift.h. Пришлось прописать его в конфигурации Project. Надеюсь, в следующей версии исправят этот глюк.

Package.swift

Компилируем, смотрим размер бинарника:
$ swift build
Building for debugging...
[7/7] Linking Application
Build complete! (39.81s)
$ size .build/x86_64-apple-macosx/debug/Application
__TEXT __DATA __OBJC others dec hex
49152 16384 0 4295081984 4295147520 10002c000
Вполне компактно, даже при сборке с отладкой: меньше 50 килобайт кода. Смотрим зависимости:
$ otool -L .build/x86_64-apple-macosx/debug/Application
.build/x86_64-apple-macosx/debug/Application:
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1600.151.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1336.0.0)
/System/Library/Frameworks/Combine.framework/Versions/A/Combine (compatibility version 1.0.0, current version 311.0.0)
/System/Library/Frameworks/DeveloperToolsSupport.framework/Versions/A/DeveloperToolsSupport (compatibility version 1.0.0, current version 21.0.15)
/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 2048.1.255, weak)
/System/Library/Frameworks/SwiftUI.framework/Versions/A/SwiftUI (compatibility version 1.0.0, current version 5.0.83)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
/usr/lib/swift/libswiftCore.dylib (compatibility version 1.0.0, current version 5.9.0)
/usr/lib/swift/libswiftCoreFoundation.dylib (compatibility version 1.0.0, current version 120.100.0)
/usr/lib/swift/libswiftCoreImage.dylib (compatibility version 1.0.0, current version 2.0.0, weak)
/usr/lib/swift/libswiftDarwin.dylib (compatibility version 1.0.0, current version 0.0.0, weak)
/usr/lib/swift/libswiftDispatch.dylib (compatibility version 1.0.0, current version 34.0.2, weak)
/usr/lib/swift/libswiftIOKit.dylib (compatibility version 1.0.0, current version 1.0.0, weak)
/usr/lib/swift/libswiftMetal.dylib (compatibility version 1.0.0, current version 341.16.1, weak)
/usr/lib/swift/libswiftOSLog.dylib (compatibility version 1.0.0, current version 4.0.0, weak)
/usr/lib/swift/libswiftObjectiveC.dylib (compatibility version 1.0.0, current version 8.0.0, weak)
/usr/lib/swift/libswiftQuartzCore.dylib (compatibility version 1.0.0, current version 3.0.0, weak)
/usr/lib/swift/libswiftUniformTypeIdentifiers.dylib (compatibility version 1.0.0, current version 790.0.0, weak)
/usr/lib/swift/libswiftXPC.dylib (compatibility version 1.0.0, current version 29.0.2, weak)
/usr/lib/swift/libswiftos.dylib (compatibility version 1.0.0, current version 1040.0.0, weak)
Обширный список, но всё это базовый рантайм MacOS нынче.

В целом смотрится очень неплохо. Надо будет какую-нибудь простую штукенцию на Swift сбацать, типа калькулятора MK-61.

Date: 2023-11-22 11:27 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi

Ой тоска.... 30 лет назад. До сих пор подташнивает.

Date: 2023-11-22 19:18 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi

Был просто ихний си. С прибамбасами и ресурсами.

Date: 2023-11-22 19:37 (UTC)
From: [personal profile] ivanrubilo
Тоже лет 20 назад писал на работе гуй на MFC.
Тоже до сих пор блевать хочется.

Qt, с другой стороны, очень нравился.

Date: 2023-11-22 21:07 (UTC)
From: [personal profile] ivanrubilo
Ну так сложно понять, вроде бы более-менее.
Удивительно, но как-то давненько, опять же, лет 15-20 назад делал для диплома прогу с гуём на C# и Win Forms или как там оно называется одному раздолбаю-коллеге на работе и как-то вполне тоже приятно было. Вообще впервые трогал и C# и Win Forms и как-то всё логично и сходу получилось без проблем.

Но я не настоящий сварщик - окошко для фреймбуффера на libSDL - это мой максимум нужды в гуях. Сейчас наверное бы вообще делал на node.js в браузере если надо гуй, весь этот Шалтай-болтай нативных гуёв маленько надоел. Ну либо вообще богопротивный Electron.

Date: 2023-11-22 21:23 (UTC)
From: [personal profile] ivanrubilo
Что-то вспомнилось:
Курсе на первом дело было. Мы тогда все со Спектрума перешли на Писи, кто через Амигу опять же, но все со временем перешли на Писи в конце-концов. На Спектруме модно было кодить только на ассемблере чтобы хоть как-то быстро что-то работало, ну и на Писи тоже сидели в Досе и на Тасме кодировали всякие видеоэффекты для демосцены.
Инета не было и один из наших кодеров начал учить Си и купил на Митинском радиорынке диск с Watcom C.
Ну и начал задвигать мол что это будущее и вообще сам Джон Кармак на этом пишет.
Ну и как-то я у него по модему это дело качаю, скорость еле-еле. И он такой мол - не копируй директории с хэдерами и либами MFC, я, говорит, тебе гарантирую что тебе в жизни это никогда не пригодится.
Хаха, лет через 5 после тех событий сидел в конторе и писал гуй на MFC для CAD для полукосмической конторы одной.

Date: 2023-11-23 03:14 (UTC)
ircicq: (Default)
From: [personal profile] ircicq
MFC (слившийся с ATL) по прежнему входит в комплект VS

Date: 2023-11-23 17:55 (UTC)
From: [personal profile] ex0_planet
Пару лет назад ковырял один вполне живой и продающийся проект на MFC, попутно пришлось кое что поресёрчить.

Самое подходящее слово для описания всего этого — "заповедник". Оно осталось примерно там же где было 20+ лет назад, правда, приросло парой новых виндовых API. Всё это обмазано какими-то custom control'ами (которые далеко не лучшие люди города делали, нет), полузаконченными theme engine на полуручном приводе... Есть какие-то коммерческие библиотеки, которые толи никто не покупает, толи никто просто не знает про них (там заходишь -- и сайт в стиле web 1.0, оч показательно). Итд итп.

Так что поддерживаться оно поддерживается, никто его не переводил в статус deprecated. Но вот то что за это время микрософты не сделали НИЧЕГО чтобы сделать его как-то проще, безопаснее или, там, удобнее — это правда.

Date: 2023-11-23 20:32 (UTC)
From: [personal profile] ivanrubilo
Ой, не напоминайте про кастом контролы, это боль была дааа. Тоже купили какую-то либу у конторы из Киева и поверх неё кастом контролы чтобы как в Автокаде… как вспомню - так вздрогну. Писали ещё как дурни без критических секций и т.д. и в какой-то момент как начало всё рассыпаться- затрахались чинить. Я из той конторы через 2 месяца сбежал.

Date: 2023-11-22 22:36 (UTC)
ircicq: (Default)
From: [personal profile] ircicq
Разумно ли в наше время писать на платформо-зависимом фреймворке?
Qt-приложение без изменений работает на Linux/Windows/macOs.

Date: 2023-11-23 18:02 (UTC)
From: [personal profile] ex0_planet
       HStack {
            Button("-") {
                proxy.decrement()
            }
            Text("\(proxy.count)")
            Button("+") {
                proxy.increment()
            }
        }


Это всё выглядит хорошо, и даже в древнем gtk каком-нибудь было (и не сильно геморройнее), но потом приходят UI-дизайнеры с вёрсткой в фигме и желают странного. Последний раз в моей практике они захотели аналог "flex-flow". На чём мы и разосрались успешно.