V úterý Google vydal Android 16. Zdrojové kódy jsou k dispozici na AOSP (Android Open Source Project). Chybí (zatím?) ale zdrojové kódy specifické pro telefony Pixel od Googlu. Projekty jako CalyxOS a GrapheneOS řeší, jak tyto telefony nadále podporovat. Nejistá je podpora budoucích Pixelů. Souvisí to s hrozícím rozdělením Googlu (Google, Chrome, Android)?
Byla vydána (𝕏) květnová aktualizace aneb nová verze 1.101 editoru zdrojových kódů Visual Studio Code (Wikipedie). Přehled novinek i s náhledy a videi v poznámkách k vydání. Ve verzi 1.101 vyjde také VSCodium, tj. komunitní sestavení Visual Studia Code bez telemetrie a licenčních podmínek Microsoftu.
V Brně na FIT VUT probíhá třídenní open source komunitní konference DevConf.CZ 2025. Vstup je zdarma, nutná je ale registrace. Na programu je celá řada zajímavých přednášek, lightning talků, meetupů a workshopů. Přednášky lze sledovat i online na YouTube kanálu konference. Aktuální dění lze sledovat na Matrixu, 𝕏 nebo Mastodonu.
Vyloučení technologií, které by mohly představovat bezpečnostní riziko pro stát, má umožnit zákon o kybernetické bezpečnosti, který včera Senát schválil spolu s novelami navazujících právních předpisů. Norma, kterou nyní dostane k podpisu prezident, počítá rovněž s prověřováním dodavatelů technologií pro stát. Normy mají nabýt účinnosti od třetího měsíce po jejich vyhlášení ve Sbírce zákonů.
Open source platforma Home Assistant (Demo, GitHub, Wikipedie) pro monitorování a řízení inteligentní domácnosti byla vydána v nové verzi 2025.6.
Po Red Hat Enterprise Linuxu a AlmaLinuxu byl v nové stabilní verzi 10.0 vydán také Rocky Linux. Přehled novinek v poznámkách k vydání.
Bylo vydáno Eclipse IDE 2025-06 aneb Eclipse 4.36. Představení novinek tohoto integrovaného vývojového prostředí také na YouTube.
Americká filmová studia Walt Disney a Universal Pictures podala žalobu na provozovatele populárního generátoru obrázků pomocí umělé inteligence (AI) Midjourney. Zdůvodňují to údajným porušováním autorských práv. V žalobě podané u federálního soudu v Los Angeles označují firmu za „bezednou jámu plagiátorství“, neboť podle nich bez povolení bezostyšně kopíruje a šíří postavy z filmů jako Star Wars, Ledové království nebo Já, padouch, aniž by do nich investovala jediný cent.
Ultra Ethernet Consortium (UEC), jehož cílem je optimalizace a další vývoj Ethernetu s důrazem na rostoucí síťové požadavky AI a HPC, vydalo specifikaci Ultra Ethernet 1.0 (pdf, YouTube).
Francouzský prezident Emmanuel Macron chce zakázat přístup na sociální sítě pro děti do 15 let. Francie podle něj tento krok udělá sama do několika měsíců, i pokud se na něm neshodnou další státy Evropské unie. Reaguje tak na úterní vraždu vychovatelky, kterou ve východofrancouzském městě Nogent pobodal 14letý mladík. Jednotlivé sociální sítě podle něj mají možnost věk ověřit a vymáhat zákaz pomocí systémů na rozpoznávání tváří.
WebKit je framework pro vykreslování HTML, jehož port je v Qt dostupný od verze 4.4.
Qt poskytuje widget QWebView
, což je z uživatelského hlediska prostor pro vykreslení webové stránky. Když jej propojíme s několika málo ovládacími prvky, tak získáme relativně schopný webový prohlížeč s podporou tabů a sotva dvěma sty řádky kódu (viz níže). Pro představu: načtení stránky spočívá ve spuštění metody load()
s argumentem URL.
Rozhraní s taby poskytuje třída QTabWidget
, jejíž použití je celkem prosté. Do rozhraní se přidávají taby metodou addTab()
, které se jako argument mj. předá widget, který bude na tabu zobrazený. Po zavolání metody setTabsClosable(true)
lze taby i zavírat. Obsluhu zavření musíte poskytnout vy sami. K dispozici máte signál tabCloseRequested()
s indexem jako argument. Ten tedy napojíte na váš slot, který se postará o korektní zničení tabu na daném indexu. Odstranění tabu obstarává metoda removeTab()
, která však nesmaže widget, který na tabu byl zobrazený, takže ve většině případů je lepší smazat tento widget, což rovněž zapříčiní zavření tabu. Přepínání tabů funguje i bez zásahů, ale v případě potřeby lze obsloužit reakcí na signál currentChanged()
s indexem nového tabu jako argument, čehož v ukázce využívám.
Při pročítání kódu si všimněte praktického použití QRegExp, o kterém jsme mluvili minule. Novinkou je zde odpojení signálů od slotů. Chceme-li jednoduše odpojit všechno, co je napojené na signály widgetu, můžeme zavolat buď metodu daného widgetu:
widget->disconnect()
nebo statickou metodu třídy QObject:
disconnect(widget, 0, 0, 0)
Pro více ukázek se podívejte do dokumentace k QObject::disconnect()
.
Při vytváření GUI projektu v Qt Creatoru nezapomeňte povolit modul QtWebKit nebo případně přidat do .pro souboru:
QT += webkit
To zajistí doplnění cest ke hlavičkovým souborům a linkování s knihovnami WebKitu.
browser.h
: API.
#ifndef BROWSER_H #define BROWSER_H #include <QMainWindow> #include <QUrl> class QLabel; class QLineEdit; class QProgressBar; class QTabWidget; class QWebView; class Browser : public QMainWindow { Q_OBJECT public: Browser(QWidget *parent = 0); private: QLabel* statusLabel; QLineEdit* urlBar; QProgressBar* loading; QTabWidget* tabs; // "web" je widget poskytující webový prohlížeč QWebView* web; private slots: void addNewTab(); void tabChanged(int); void closeTab(int); void setLoadingStatus(bool); void loadUrl(); void setUrl(QUrl); void changeTitle(QString); void goBack(); void goForward(); void reload(); void stop(); }; #endif // BROWSER_H
browser.cpp
: Z důvodu přehlednosti jsem navrhl GUI ručně, místo použití Designeru, který by mi v tomto případě práci ani příliš neusnadnil. Novinkou je zde použití qobject_cast
místo static_cast
pro přetypování. qobject_cast
se chová podobně jako dynamic_cast
v C++, ale má několik výhod; například nepotřebuje RTTI. Funguje pouze na objekty, které přímo či nepřímo dědí QObject
a jsou deklarovány s makrem Q_OBJECT
.
#include "browser.h" #include <QAction> #include <QKeySequence> #include <QLabel> #include <QLineEdit> #include <QProgressBar> #include <QPushButton> #include <QRegExp> #include <QStatusBar> #include <QStyle> #include <QTabWidget> #include <QToolBar> #include <QWebView> Browser::Browser(QWidget *parent) : QMainWindow(parent), web(0) { // vytvoříme widgety: // políčko pro URL urlBar = new QLineEdit; // rozhraní s taby tabs = new QTabWidget; // tlačítko napravo od tabů pro vytvoření nového QPushButton* newTabBtn = new QPushButton(style()->standardIcon(QStyle::SP_FileDialogNewFolder), "", tabs); // text ve stavovém řádku statusLabel = new QLabel; // průběh načítání stránky ve stavovém řádku loading = new QProgressBar; // panel nástrojů (obsahující ovládací prvky prohlížeče) QToolBar* toolBar = addToolBar(tr("Controls")); // přidáme akce do panelu nástrojů a rovnou je napojíme na sloty toolBar->addAction(style()->standardIcon(QStyle::SP_ArrowBack), tr("Back"), this, SLOT(goBack())); toolBar->addAction(style()->standardIcon(QStyle::SP_ArrowForward), tr("Forward"), this, SLOT(goForward())); toolBar->addAction(style()->standardIcon(QStyle::SP_BrowserReload), tr("Reload"), this, SLOT(reload())); toolBar->addAction(style()->standardIcon(QStyle::SP_BrowserStop), tr("Stop"), this, SLOT(stop())); // uložíme si ukazatel akce pro vytvoření nového tabu QAction* actionAddTab = toolBar->addAction(style()->standardIcon(QStyle::SP_FileDialogNewFolder), tr("New tab"), this, SLOT(addNewTab())); // a přiřadíme jí klávesovou zkratku actionAddTab->setShortcut(QKeySequence("Ctrl+T")); // přidáme adresní řádek toolBar->addWidget(urlBar); toolBar->addAction(style()->standardIcon(QStyle::SP_CommandLink), tr("Go"), this, SLOT(loadUrl())); // propojíme tlačítko pro přidání tabů connect(newTabBtn, SIGNAL(clicked()), this, SLOT(addNewTab())); // reakce na změnu aktuálního tabu connect(tabs, SIGNAL(currentChanged(int)), this, SLOT(tabChanged(int))); // reakce na zavření daného tabu connect(tabs, SIGNAL(tabCloseRequested(int)), this, SLOT(closeTab(int))); // reakce stisknutí Enteru v adresním řádku connect(urlBar, SIGNAL(returnPressed()), this, SLOT(loadUrl())); // přidáme tlačítko vedle tabů tabs->setCornerWidget(newTabBtn); // povolíme přesouvání tabů tabs->setMovable(true); // nastavíme pevnou šířku ukazateli průběhu loading->setFixedWidth(100); // nastavíme hlavnímu oknu: // hlavní widget (rozhraní s taby) setCentralWidget(tabs); // a stavový řádek setStatusBar(new QStatusBar); // popisek ve stavovém řádku nebude zvětšovat okno statusBar()->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); // přidáme do stavového řádku popisek statusBar()->addWidget(statusLabel); // a ukazatel průběhu statusBar()->addPermanentWidget(loading); // vytvoříme první tab addNewTab(); // nastavíme do adresního řádku adresu abclinuxu urlBar->setText("http://www.abclinuxu.cz"); // a stránku načteme loadUrl(); // nastavíme výchozí velikost okna resize(950, 700); } // vytvoří nový tab void Browser::addNewTab() { // přidáme tab int index = tabs->addTab(new QWebView, tr("New tab")); // pokud máme víc než 1 tab, if(tabs->count() > 1) { // přepneme se na tab nově vytvořený tabs->setCurrentIndex(index); // a povolíme zavírání tabů tabs->setTabsClosable(true); } // vyprázdníme adresní řádek urlBar->setText(""); // vynulujeme ukazatel průběhu načítání loading->reset(); // nastavíme titulek okna setWindowTitle("WebKit browser"); } void Browser::tabChanged(int index) { // pokud si zavřeme poslední tab, dostaneme index o hodnotě -1 // a k tomu by nemělo dojít, takže v debug. verzi shodíme program Q_ASSERT(index >= 0); // v obyčejné verzi se problém pokusíme obejít přidáním nového tabu if(index < 0) { addNewTab(); return; } if(web) { // odpojíme tab ze kterého přepínáme od GUI slotů (viz níže) disconnect(web, SIGNAL(urlChanged(QUrl)), this, SLOT(setUrl(QUrl))); disconnect(web, SIGNAL(titleChanged(QString)), this, SLOT(changeTitle(QString))); disconnect(web, SIGNAL(loadProgress(int)), loading, SLOT(setValue(int))); disconnect(web, SIGNAL(loadFinished(bool)), this, SLOT(setLoadingStatus(bool))); disconnect(web->page(), SIGNAL(linkHovered(QString,QString,QString)), statusLabel, SLOT(setText(QString))); } // získáme prohlížeč aktuálního tabu web = qobject_cast<QWebView*>( tabs->widget(index) ); // a napojíme jej na GUI: // projeví změnu URL z prohlížeče do adresního řádku connect(web, SIGNAL(urlChanged(QUrl)), this, SLOT(setUrl(QUrl))); // nastaví titulek stránky jako titulek okna a tabu connect(web, SIGNAL(titleChanged(QString)), this, SLOT(changeTitle(QString))); // zobrazuje průběh načítání connect(web, SIGNAL(loadProgress(int)), loading, SLOT(setValue(int))); connect(web, SIGNAL(loadFinished(bool)), this, SLOT(setLoadingStatus(bool))); // když kurzor najede na odkaz, zobrazíme jeho cíl ve stavovém řádku connect(web->page(), SIGNAL(linkHovered(QString,QString,QString)), statusLabel, SLOT(setText(QString))); // načteme správnou adresu do adresního řádku setUrl(web->url()); // vyprázdníme text ve stavovém řádku statusLabel->setText(""); // změníme titulek okna setWindowTitle(tabs->tabText(tabs->currentIndex())); } // zavře tab s daným indexem void Browser::closeTab(int index) { // v případě, že zavíráme aktuální tab, nastavíme web na 0, aby se tabChanged() nepokoušelo // odpojovat neexistující widget if(tabs->widget(index) == web) web = 0; // smazání widgetu zavře tab delete tabs->widget(index); // pokud po zavření akt. tabu zbyl už jen jeden, zakážeme jeho zavření if(tabs->count() < 2) tabs->setTabsClosable(false); } // informuje ve stavovém řádku o úspěšnosti načtení stránky void Browser::setLoadingStatus(bool ok) { QString text = tr("OK"); if(!ok) text = tr("Loading failed"); statusLabel->setText(text); } // načte adresu zadanou v adresním řádku void Browser::loadUrl() { QString url = urlBar->text(); // pokud daná adresa neobsahuje protokol, předpokládáme http:// if(!url.contains(QRegExp("^[a-z]+://"))) url.prepend("http://"); // načteme připravenou adresu web->load(QUrl(url, QUrl::TolerantMode)); } // nastaví dané URL do adresního řádku void Browser::setUrl(QUrl url) { // z URL odstraníme přihlašovací informace urlBar->setText(url.toString(QUrl::RemoveUserInfo)); } // nastaví titulek okna a aktuálního tabu void Browser::changeTitle(QString title) { if(title.isEmpty()) title = web->url().toString(QUrl::RemoveUserInfo); setWindowTitle(title); tabs->setTabText(tabs->currentIndex(), title); } // "Zpět" v prohlížeči void Browser::goBack() { web->back(); } // "Vpřed" v prohlížeči void Browser::goForward() { web->forward(); } // "Obnovit" v prohlížeči void Browser::reload() { web->reload(); } // "Stop" v prohlížeči void Browser::stop() { web->stop(); }
Zdrojáky si můžete stáhnout v archívu browser.tar.bz2.
Phonon je multiplatformní multimediální framework, který umožňuje přehrávat multimédia přes různé enginy, jako je Xine a GStreamer. Byl vyvinut pro použití v KDE 4 a Qt jej obsahuje od verze 4.4. Phonon pro KDE 4 je kompatibilní s tím, který je součástí Qt, takže pokud máte KDE 4, tak do .pro souboru přidejte kromě
QT += phonon
což zajistí linkování s tímto modulem, navíc ještě
INCLUDEPATH += /usr/include/KDE
Tímto umožníte chod vašeho Qt programu, který používá Phonon, uživatelům KDE 4 (na unixech).
Základní použití Phononu pro přehrávání zvuku je velice jednoduché. Stačí si vytvořit objekt reprezentující zvukový výstup (Phonon::AudioOutput
), objekt pro samotné přehrávání (Phonon::MediaObject
) a následně tyto dva objekty propojit pomocí Phonon::createPath()
. Teď už jen předáme objektu pro přehrávání nějaké URL pomocí setCurrentSource()
a můžeme zavolat play()
pro zahájení přehrávání. Tolik tedy k Phononu.
Když chcete za běhu programu zobrazit okno (např. dialog s nastavením), postup je podobný jako při zobrazování hlavního okna metodou QWidget::show()
. Teď před námi ovšem stojí jedno rozhodnutí: bude naše okno modální? Modální okno blokuje přístup k ostatním oknům programu, zatímco nemodální se otevře jako nezávislé okno, obvykle s vlastní položkou v pruhu úloh na panelu. Tato vlastnost se nastavuje metodou setWindowModality()
a dává smysl pochopitelně jen u oken. Modálnost se rozděluje na dvě varianty, buď okno blokuje přístup k celému programu (výchozí chování) nebo jen k rodičovskému oknu.
Často se místo běžného okna (QWidget
) hodí použít dialog (QDialog
), jehož použití uvidíte v následující ukázce, kde nám poslouží jako dialog s nastavením. Existuje několik možností jak dialog zobrazit:
Metoda (slot) | Modálnost | Okamžitý return |
show() | respektuje nastavení, výchozí = nemodální | ano |
exec() | vynutí modální dialog | ne, vrací návratovou hodnotu |
open() | vynutí modální dialog blokující rodičovské okno | ano |
Jaký režim tedy používat - modální, nebo nemodální? Je to na vás. Vhodné je rozhodnout se na základě účelu daného dialogu. Pokud jde o velice jednoduchý dialog, jako třeba ten v mé ukázce, tak tam asi modálnost nikomu vadit nebude, ale pokud půjde třeba o rozsáhlejší dialog s nastavením, které bude možné aktivovat tlačítkem "Použít", potom stojí za zvážení použití nemodální okno. Důvod, proč někteří programátoři používají modální okna i tam, kde se přiliš nehodí, může být fakt, že nemodální jsou trochu složitější na naprogramování, jelikož slot pro zobrazení okna ihned vrací a programátor musí sám hlídat, aby stejný dialog nebylo možné vyvolat dvakrát. Rozdíl si můžete prohlédnout v ukázce obsažené v dokumentaci.
main.cpp
: Nastavíme název programu a organizace (nebo doménu) kvůli nastavení (QSettings
) a Phononu, který to vyžaduje pro D-Bus.
#include <QApplication> #include "player.h" int main(int argc, char *argv[]) { QApplication a(argc, argv); a.setApplicationName("SimplePhononPlayer"); a.setOrganizationDomain("watzke.cz"); Player w; w.setWindowTitle(a.applicationName()); w.show(); return a.exec(); }
player.h
: API.
#ifndef PLAYER_H #define PLAYER_H #include <QMainWindow> #include <QSettings> #include <Phonon/AudioOutput> #include <Phonon/MediaObject> class QLabel; using namespace Phonon; class Player : public QMainWindow { Q_OBJECT public: Player(QWidget *parent = 0); ~Player(); private: AudioOutput* ao; MediaObject* mo; QLabel* statusLabel; QSettings settings; private slots: void openSettings(); void playFile(); void setStatusMessage(Phonon::State, Phonon::State); }; #endif // PLAYER_H
player.cpp
: Zde je novinkou použití makra Q_UNUSED
. Nestojí za tím žádná věda, slouží to čistě jen k umlčení kompilátoru, který by si jinak stěžoval na nevyužitou proměnnou, které díky tomuto můžete nastavit smysluplný název.
#include "player.h" #include "settings.h" #include <QFileDialog> #include <QLabel> #include <QToolBar> #include <QStatusBar> #include <QStyle> #include <Phonon/VolumeSlider> #include <Phonon/SeekSlider> Player::Player(QWidget *parent) : QMainWindow(parent) { // vytvoříme zvukový výstup, ao = new AudioOutput(this); // objekt pro přehrávání multimédií mo = new MediaObject(this); // a propojíme je spolu Phonon::createPath(mo, ao); // vytvoříme a naplníme panel nástrojů QToolBar* toolBar = addToolBar(tr("Controls")); toolBar->addAction(style()->standardIcon(QStyle::SP_DialogOpenButton), tr("Select a file"), this, SLOT(playFile())); toolBar->addAction(style()->standardIcon(QStyle::SP_ComputerIcon), tr("Settings"), this, SLOT(openSettings())); toolBar->addSeparator(); toolBar->addAction(style()->standardIcon(QStyle::SP_MediaStop), tr("Stop"), mo, SLOT(stop())); toolBar->addAction(style()->standardIcon(QStyle::SP_MediaPause), tr("Pause"), mo, SLOT(pause())); toolBar->addAction(style()->standardIcon(QStyle::SP_MediaPlay), tr("Play"), mo, SLOT(play())); toolBar->addSeparator(); // přidáme widget pro ovládání hlasitosti, který Phonon nabízí toolBar->addWidget(new VolumeSlider(ao)); // vynutíme si text vedle ikon toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); // jako hlavní widget nastavíme posuvník, který je součástí Phononu setCentralWidget(new SeekSlider(mo)); // vytvoříme stavový řádek setStatusBar(new QStatusBar); // a na něm textovou oblast pro zobrazování informací statusBar()->addWidget(statusLabel = new QLabel(tr("Idle"))); // dlouhé hlášky ve stavovém řádku nebudou roztahovat okno statusBar()->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); // skryjeme úchyt pro změnu velikosti okna statusBar()->setSizeGripEnabled(false); // změna stavu přehrávání vyvolá zobrazení patřičné informace connect(mo, SIGNAL(stateChanged(Phonon::State,Phonon::State)), this, SLOT(setStatusMessage(Phonon::State,Phonon::State))); // nastavíme výchozí velikost okna na doporučené hodnoty resize(sizeHint()); // obnovíme geometrii okna; pokud není uložená, použijeme aktuální (žádná změna) restoreGeometry(settings.value("geometry", saveGeometry()).toByteArray()); } Player::~Player() { // pokud je nastavené zapamatování geometrie okna, uložíme ji, jinak záznam smažeme if(settings.value("Settings/rememberGeometry", false).toBool()) settings.setValue("geometry", saveGeometry()); else settings.remove("geometry"); } // otevře okno s nastavením void Player::openSettings() { // vytvoříme okno Settings window(settings); // vynutíme modálnost a zobrazíme okno window.exec(); } // spustí přehrávání zvoleného souboru void Player::playFile() { QString fileName = QFileDialog::getOpenFileName(this, tr("Select an audio file")); if(fileName.isEmpty()) return; mo->setCurrentSource(fileName); mo->play(); } // nastaví informaci o stavu do stavového řádku void Player::setStatusMessage(Phonon::State newState, Phonon::State prevState) { Q_UNUSED(prevState); QString status; switch(newState) { case Phonon::LoadingState: status = tr("Loading") + "..."; break; case Phonon::StoppedState: status = tr("Stopped"); break; case Phonon::PlayingState: status = tr("Playing ") + mo->currentSource().fileName(); break; case Phonon::BufferingState: status = tr("Buffering") + "..."; break; case Phonon::PausedState: status = tr("Paused"); break; case Phonon::ErrorState: status = tr("Error: ") + mo->errorString(); break; default: break; } if(!status.isEmpty()) { statusLabel->setText(status); // když uživatel najede kurzorem na informaci, zobrazí se mu v tooltipu, // což se může hodit, když se informace nevejde do okna celá statusLabel->setToolTip(status); } }
settings.h
: API.
#ifndef SETTINGS_H #define SETTINGS_H #include <QDialog> #include <QSettings> class QCheckBox; class Settings : public QDialog { Q_OBJECT public: Settings(QSettings& settings); private: QSettings* m_settings; QCheckBox* geometryCb; protected: void accept(); }; #endif // SETTINGS_H
settings.cpp
:
#include "settings.h" #include <QCheckBox> #include <QDialogButtonBox> #include <QVBoxLayout> Settings::Settings(QSettings& settings) { // uložíme si ukazatel na instanci objektu s nastavením m_settings = &settings; // vytvoříme zaškrtávací pole geometryCb = new QCheckBox(tr("Remember main window's geometry")); // a std. dialogová tlačítka, QDialogButtonBox* dialogButtons = new QDialogButtonBox; // konkrétně "OK" a "Zrušit" dialogButtons->setStandardButtons(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); // propojíme signály tlačítek s patřičnými sloty connect(dialogButtons, SIGNAL(accepted()), this, SLOT(accept())); connect(dialogButtons, SIGNAL(rejected()), this, SLOT(reject())); // vytvoříme rozložení ovl. prvků QVBoxLayout* layout = new QVBoxLayout(this); layout->addWidget(geometryCb); layout->addWidget(dialogButtons); // zaškrtávací pole odráží aktuální hodnotu nastavení if(m_settings->value("Settings/rememberGeometry", false).toBool()) geometryCb->setChecked(true); // titulek okna setWindowTitle(tr("Settings")); } // uloží nastavení a zavře dialog void Settings::accept() { m_settings->setValue("Settings/rememberGeometry", geometryCb->isChecked()); done(QDialog::Accepted); }
Zdrojáky si můžete stáhnout v archívu okna.tar.bz2.
V příštím díle si ukážeme, jak vytvářet překlady programů a jak k programům přibalit různá data (např. obrázky, zvuky, atp.).
Nástroje: Tisk bez diskuse
Tiskni
Sdílej:
Geniální. S Qt si hraju už dlouho a oceňuji, že mi někdo strčí pod nos takovou studnici nápadů. Tahat tyhle věci někde ze zdrojáků hotových aplikací je únavné - tady mám nápad přímo a bez příkras
Fakt skvělý seriál. Dík za vysvětlení "modálnosti". Hned využiji.