Pluginy aplikacji Qt5

Tworzenie systemu pluginów aplikacji działającej na różnych systemach wymagało zawsze wielokrotnej implementacji tego samego – dla każdego systemu oddzielnie, głównie z powodu sposobu dynamicznego ładowania bibliotek. W Qt jednak jest inaczej – framework ten udostępnia bardzo sprawnie działający system pluginów.

Na początek zdefiniujmy interfejs (API):

#ifndef DESIGNLAYER_API_H
#define DESIGNLAYER_API_H

#include <QString>
#include <QtPlugin>

namespace apophysis
	{		
	class DesignLayerAPI
		{
		public:
			virtual ~DesignLayerAPI() {};
			virtual QString getName() = 0;
		};
	}

#define DesignLayerAPI_iid "org.apophysis.DesignLayerAPI"

Q_DECLARE_INTERFACE(apophysis::DesignLayerAPI, DesignLayerAPI_iid)

#endif // DESIGNLAYER_API_H

Widzimy tu wirtualną klasę, definiującą metodę getName(). Po definicji API, następuje definicja interfejsu, po którym Qt będzie rozpoznawać nasze pluginy, implementujące te API.

Czas na plugin. Deklarujemy klasę implementującą API:

#ifndef DESIGNLAYER_IMAGE_H
#define DESIGNLAYER_IMAGE_H

#include <QObject>
#include <QString>
#include <DesignLayerAPI.h>

class designLayerImage : public QObject, apophysis::DesignLayerAPI
	{
	Q_OBJECT
		Q_PLUGIN_METADATA(IID "org.apophysis.DesignLayerAPI" FILE "designLayerImage.json")
		Q_INTERFACES(apophysis::DesignLayerAPI)

	public:
		designLayerImage();
		QString getName() Q_DECL_OVERRIDE;
	private:
		QString theName;
	};

#endif // DESIGNLAYER_IMAGE_H

Oprócz deklaracji metod zadeklarowanych w interfejsie, widzimy tu jeszcze jedną rzecz: definicję metadanych. Po co to? Metadane pluginu można odczytać bez faktycznego ładowania biblioteki. Zapisujemy je w pliku JSON, np:

{
"type"			:	"Layer",
"name"			:	"Image layer plugin",
"shortname"		:	"ImageLayer",
"version"		:	"0.1",
"author"		:	"Apophysis internal",
"description"	:	"Used mainly for backgrounds: solid colour, gradients, images, etc."
}

Właściwa implementacja jest oczywista, żadnej magii tu nie ma:

#include "designLayerImage.h"

designLayerImage::designLayerImage()
	:
	theName(QLatin1Literal("Unnamed image layer"))
	{
	}

QString designLayerImage::getName()
	{
	return theName;
	}

OK, mamy plugin. Jak go użyć? Na szczęście, to również jest proste (i przenośne! Na pewno działa bez zmian pod Windows i Linux, pod OSX nie sprawdzałem).

Najpierw sprawdzimy w działaniu system metadanych. Załóżmy, że pluginy mamy w podkatalogu plugins (niespodzianka!):

#include <QPluginLoader>
#include <DesignLayerAPI.h>

void listPlugins()
	{
	QDir pluginsDir(qApp->applicationDirPath());
	pluginsDir.cd(QLatin1Literal("plugins"));
	foreach(QString fileName, pluginsDir.entryList(QDir::Files, QDir::Name))
		{
		QString libExt;
#if defined(Q_OS_WIN)
		libExt = QLatin1Literal("dll");
#elif defined(Q_OS_MAC)
		libExt = QLatin1Literal("bundle");
#elif defined(Q_OS_LINUX)
		libExt = QLatin1Literal("so");
#endif
		if (QFileInfo(fileName).suffix().toLower() == libExt)
			{
			QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName));
			qDebug() << "File:" << fileName;
			QJsonObject jsonMetaData = pluginLoader.metaData().value(QLatin1Literal("MetaData")).toObject();
			qDebug() << jsonMetaData.value(QLatin1Literal("type")).toString();
			qDebug() << jsonMetaData.value(QLatin1Literal("name")).toString();
			qDebug() << jsonMetaData.value(QLatin1Literal("shortname")).toString();
			qDebug() << jsonMetaData.value(QLatin1Literal("version")).toString();
			qDebug() << jsonMetaData.value(QLatin1Literal("author")).toString();
			qDebug() << jsonMetaData.value(QLatin1Literal("description")).toString();
			}
		}
	}

W zasadzie moglibyśmy się obejść bez sekcji sprawdzania rozszerzenia plików, ale używam tego w razie, gdyby w katalogu były jeszcze jakieś inne pliki (ot, chociażby PDB).

OK, czas na crème de la crème, czyli stworzenie instancji klasy zaimplementowanej w pluginie:

void apophysis::Design::addLayer(const QString & aLayerType)
	{
	QDir pluginsDir(qApp->applicationDirPath());
	pluginsDir.cd(QLatin1Literal("plugins"));
	foreach(QString fileName, pluginsDir.entryList(QDir::Files, QDir::Name))
		{
		QString libExt;
#if defined(Q_OS_WIN)
		libExt = QLatin1Literal("dll");
#elif defined(Q_OS_MAC)
		libExt = QLatin1Literal("bundle");
#elif defined(Q_OS_LINUX)
		libExt = QLatin1Literal("so");
#endif
		if (QFileInfo(fileName).suffix().toLower() == libExt)
			{
			QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName));
			QString plugintype = pluginLoader.metaData().value(QLatin1Literal("MetaData")).toObject().value(QLatin1Literal("type")).toString();
			QString pluginshortname = pluginLoader.metaData().value(QLatin1Literal("MetaData")).toObject().value(QLatin1Literal("shortname")).toString();
			if ((plugintype == QLatin1Literal("Layer")) && (pluginshortname == aLayerType))
				{
				QObject *plugin = pluginLoader.instance();
				if (plugin)
					{
					apophysis::DesignLayerAPI *layer = qobject_cast<apophysis::DesignLayerAPI *>(plugin);
					if (layer)
						theLayers.append(layer);
					}
				}
			}
		}
	}

Jak widać, zarówno tworzenie, jak i używanie pluginów w tym systemie jest trywialne - i jest całkowicie przenośne.

Happy coding!

Facebooktwittergoogle_plusredditpinterestlinkedinmail
twitterlinkedin

Git a automatyczne budowanie Jenkinsem

Do tej pory, na cele Continuous Integration, w Jenkinsie miałem ustawione sprawdzanie repozytorium Git (opcja SCM polling) co 15 minut. W zasadzie to działało, ale bywały momenty, że owe 15 minut było okresem zbyt długim. Można oczywiście (nikt nie broni…) ustawić okres sprawdzania na * * * * *, czyli sprawdzanie co minutę, ale to również nie jest dobre rozwiązanie, z oczywistych powodów. Dlatego też postanowiłem powiesić akcję odpytywania Gita przez Jenkinsa na haku, żeby automagicznie każdy push od developera powodował start Jenkinsa.

Oczywiście, Jenkins musi być wyposażony we wtyczkę Git.

Zaczynamy od próby ręcznego zdalnego wywołania Jenkinsa. Otóż wtyczka Git otwiera takową drogę właśnie na okoliczność notyfikacji o nowym commicie. Jak czytamy w dokumentacji wtyczki, można powiadomić Jenkinsa o konkretnym commicie, ale ja na cele Continuous Integration i tak chcę pobrać najnowsze w danej chwili źródła, aby sprawdzić integrację poszczególnych prac właśnie. Dlatego użyjemy najprostszej formy wywołania wtyczki, która spowoduje wymuszenie odpytania repozytorium o zmiany.

Na początek, w konfiguracji zadania Jenkinsowego, usuwamy wszelkie terminarze, jakie mieliśmy – ale samą opcję Poll SCM musimy pozostawić włączoną (ale pustą):

Jenkins SCM Polling

Następnie, będąc na serwerze gitowym, instalujemy pakiet curl, jeśli go tam jeszcze nie ma. Gdy już go mamy, wywołujemy testową komendę:

curl http://moj.jenkins.com:8080/git/notifyCommit?url=git@moj.git.com:WspanialeRepo

Ścieżka do repozytorium powinna być dokładnie taka sama, jaką podajemy przy jego klonowaniu. Jeśli wszystko pójdzie dobrze, przeczytamy, że zostało zlecone odpytanie repozytorium. Sprawdzamy w logach Jenkinsa Git Polling Log, czy rzeczywiście tak było.

Jeśli wszystko zadziałało, czas ten proces zautomatyzować. Aby wywołać jakąś akcję po każdym pushu od developera, musimy zdefiniować hak post-receive. W katalogu repozytorium znajduje się podkatalog hooks. Tworzymy nowy plik i nadajemy mu właściwe uprawnienia:

touch post-receive
chown git:git post-receive
chmod 775 post-receive

i edytujemy go:

#!/bin/sh
curl http://moj.jenkins.com:8080/git/notifyCommit?url=git@moj.git.com:WspanialeRepo

Od tej pory, za każdym razem, gdy developer zrobi pusha, Jenkins wykona swoją robotę 🙂 A my cieszymy się z Continuous Integration 😉

Facebooktwittergoogle_plusredditpinterestlinkedinmail
twitterlinkedin

Android – dobór koloru tekstu pod kolor tła

Gdy dodajemy tekst na jakimś tle, którego kolor ustawiany jest dynamicznie, mamy problem: jaki kolor tekstu? Czarny czy biały?

Aby rozwiązać ten problem, należy sprawdzić, jak jasny jest kolor tła. Problem w tym, że wpływ poszczególnych komponentów RGB na ogólną jasność jest różna, stąd trzeba ową jasność policzyć uwzględniając tę zasadę. Ja użyłem dość popularnej w internecie formuły – i spisuje się naprawdę dobrze.

    /**
     * Find the right color of the text, depending on the color of the background.
     * @param aColor Color, to which we're adjusting by contrast.
     * @return Black or white color.
     */
    public int getBlackOrWhite(int aColor)
    {
        int red = (aColor >> 16) & 0xFF;
        int green = (aColor >> 8) & 0xFF;
        int blue = (aColor >> 0) & 0xFF;
        int result = 0;
        if (((double)red*0.299 + (double)green*0.587 + (double)blue*0.114) > 186)
            result = Color.BLACK;
        else
            result = Color.WHITE;
        return result;
    }

Parametrem wejściowym jest kolor tła, wartość zwracana to kolor czarny lub biały. Formuła doboru wygląda na dobrą, bo spośród tych kolorów, które przetestowałem, wybór: czarny-biały zawsze był trafny, stąd mogę ją z czystym sumieniem polecić – co też czynię.

Facebooktwittergoogle_plusredditpinterestlinkedinmail
twitterlinkedin

Zmiana parametrów ekranu z C++ pod Windows 7

Czasem zachodzi potrzeba zmiany ustawień monitora z poziomu aplikacji. Czasem chodzi nam o rozdzielczość, czasem o odświeżanie. Mi akurat chodziło o odświeżanie – aplikacja wyświetla materiał video w różnych formatach i potrzebowałem dostosować monitor do materiału, aby osiągnąć jak najlepszą synchronizację.

Moja aplikacja jest w Qt (a jakże), więc zaczynam od sprawdzenia, na którym monitorze jest wyświetlona moja aplikacja:

int monitor = QApplication::desktop()->screenNumber(ui.centralWidget);

Aby napisać działającą poprawnie metodę zmieniającą parametry wyświetlania pod Windows 7, musimy podać dokładną nazwę urządzenia. Podawanie NULL, która wg dokumentacji MSDN powinna zadziałać na domyślnym urządzeniu, pod Windows 7 kończy się błędem niepoprawnego parametru. Dlatego najpierw listujemy wszystkie dostępne urządzenia – i jeśli jest to nasze urządzenie, pobieramy jego aktualne ustawienia i modyfikujemy je:

DEVMODE winMode;
DISPLAY_DEVICE displayDevice;
memset(&displayDevice, 0, sizeof(DISPLAY_DEVICE));
displayDevice.cb = sizeof(displayDevice);
DWORD iDevNum = 0;
if (EnumDisplayDevices(NULL, iDevNum, &displayDevice, EDD_GET_DEVICE_INTERFACE_NAME))
{
	if (EnumDisplaySettings(displayDevice.DeviceName, ENUM_CURRENT_SETTINGS, &winMode) != 0)
	{
		winMode.dmPelsWidth = aWidth;
		winMode.dmPelsHeight = aHeight;
		winMode.dmDisplayFrequency = aFreq;
		ChangeDisplaySettingsEx(displayDevice.DeviceName, &winMode, NULL, CDS_NORESET | CDS_UPDATEREGISTRY, NULL);
	}
}

Wg dokumentacji, po użyciu ChangeDisplaySettingsEx powinniśmy jeszcze spowodować rozesłanie komunikatu do wszystkich aplikacji, wywołując ją jeszcze raz w ten sposób:

ChangeDisplaySettings(NULL, 0);

Składając wszystko do kupy, napisałem sobie metodę, która bierze parametry ustawień (rozdzielczość i odświeżanie) i sprawdza, czy podałem wartość większą od zera (gdybym chciał ustawić tylko niektóre z tych parametrów):

//! Change display (monitor) settings. If you don't want to change any of the values, pass 0 for them.
static bool setCurrentDisplayMode(int aWidth, int aHeight, int aFreq, int aDisplay = 0);
bool sysWindowsApi::setCurrentDisplayMode(int aWidth, int aHeight, int aFreq, int aDisplay)
{
	bool ret = false;
	DEVMODE winMode;
	DISPLAY_DEVICE displayDevice;
	memset(&displayDevice, 0, sizeof(DISPLAY_DEVICE));
	displayDevice.cb = sizeof(displayDevice);
	if (EnumDisplayDevices(NULL, aDisplay, &displayDevice, EDD_GET_DEVICE_INTERFACE_NAME))
	{
		if (EnumDisplaySettings(displayDevice.DeviceName, ENUM_CURRENT_SETTINGS, &winMode) != 0)
		{
			if (aWidth > 0) winMode.dmPelsWidth = aWidth;
			if (aHeight > 0) winMode.dmPelsHeight = aHeight;
			if (aFreq > 0) winMode.dmDisplayFrequency = aFreq;
			if (ChangeDisplaySettingsEx(displayDevice.DeviceName, &winMode, NULL, CDS_NORESET | CDS_UPDATEREGISTRY, NULL) == DISP_CHANGE_SUCCESSFUL)
			{
				ChangeDisplaySettings(NULL, 0);
				ret = true;
			}
		}
	}
	return ret;
}

Z głównej aplikacji wywołuję ją np. tak:

sysWindowsApi::setCurrentDisplayMode(0, 0, 60, QApplication::desktop()->screenNumber(ui.centralWidget));

powoduje to zmianę tylko częstotliwości odświeżania, pozostawiając rozdzielczość bez zmian – i tylko na tym monitorze, na którym znajduje się nasza aplikacja.

Facebooktwittergoogle_plusredditpinterestlinkedinmail
twitterlinkedin

Fullscreen widget w Qt4

Stworzenie widgeta pełnoekranowego w Qt4 niby jest proste, ale… okazuje się, że po drodze czyha na nas kilka pułapek. Postaram się was przez nie przeprowadzić…

W moim przypadku, mam aplikację, która wyświetla strumień video (akurat ze specjalnej karty, ale to kompletnie nieważne – może Ty chcesz oglądać np. film z pliku). Idąc za niepisanym standardem, postanowiłem opcję przejścia do fullscreen położyć na klawiszu <F11>.

QAction *theFullscreenAction = new QAction(this);
theFullscreenAction->setShortcut(QKeySequence(Qt::Key_F11));
theFullscreenAction->setCheckable(true);
connect(theFullscreenAction, SIGNAL(toggled(bool)), this, SLOT(toggleFullscreen(bool)));
addAction(theFullscreenAction);

Aby przełączyć widget, trzeba zapamiętać kilka jego właściwości, aby było do czego wracać a następnie go przełączyć. Przy powrocie, oczywiście należy odtworzyć wszystkie właściwości.

QWidget *videoFrameParent;
QLayout *videoFrameParentLayout;
Qt::WindowFlags videoFrameFlags;
QSize videoFrameSize;
void myApp::toggleFullscreen(bool bFullscreen)
	{
	if (bFullscreen)
		{
		videoFrameParent = ui.videoFrame->parentWidget();
		videoFrameParentLayout = ui.videoFrame->parentWidget()-&gt;layout();
		videoFrameFlags = ui.videoFrame->windowFlags();
		videoFrameSize = ui.videoFrame->size();

		// untie it...
		ui.videoFrame->setParent(NULL);
		ui.videoFrame->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
		ui.videoFrame->showMaximized();
		}
	else
		{
		// bring it back!
		ui.videoFrame->resize(videoFrameSize);
		ui.videoFrame->overrideWindowFlags(videoFrameFlags);
		ui.videoFrame->setParent(videoFrameParent);
		videoFrameParentLayout>addWidget(ui.videoFrame);
		ui.videoFrame->show();
		}
	}

Odpalamy aplikację i testujemy… pierwsze, co się rzuca, to nie działa klawisz powrotu. Chwila zabawy i – działa, ale trzeba Alt-Tabem przejść do osieroconego okna aplikacji głównej. Rozwiązanie jest dość proste: wystarczy naszą akcję dodać również do naszego widgetu. Dlaczego? Dlatego, że po jego wydziedziczeniu (setParent(NULL);) aplikacja główna przestaje otrzymywać zdarzenia klawiatury, gdy nasz widget ma fokus.

ui.videoFrame->addAction(theFullscreenAction);

Kolejny problem objawia się w momencie, gdy mamy konfigurację wielomonitorową i wywołamy akcję w momencie, gdy aplikacja będzie na monitorze innym, niż główny. Okaże się, że nasz fullscreen włączy się na ekranie głównym, pozostawiając osieroconą i ogołoconą aplikację główną tam, gdzie była.

Tu niestety musimy kilka linijek kodu spędzić na rozwiązanie tego problemu… sprawdzimy najpierw, gdzie nasza aplikacja się znajduje, a następnie (po wydziedziczeniu!) przesuniemy okno tam ręcznie, po czym dopiero uruchomimy showMaximized().

// untie it...
ui.videoFrame->setParent(NULL);
ui.videoFrame->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);

// hack for showing it on a screen, where the app is (in a multimonitor setup)
QDesktopWidget *dw = QApplication::desktop();
QRect drect = dw->screenGeometry(dw->screenNumber(QCursor::pos()));
int desk_x = drect.width();
int desk_y = drect.height();
int x = ui.videoFrame->width();
int y = ui.videoFrame->height();
ui.videoFrame->move(desk_x / 2 - x / 2 + drect.left(), desk_y / 2 - y / 2 + drect.top());

// yeah!
ui.videoFrame->showMaximized();

Kolejny test i… działa! 🙂 Poniżej kod w komplecie:

QWidget *videoFrameParent;
QLayout *videoFrameParentLayout;
Qt::WindowFlags videoFrameFlags;
QSize videoFrameSize;
QAction *theFullscreenAction = new QAction(this);
theFullscreenAction->setShortcut(QKeySequence(Qt::Key_F11));
theFullscreenAction->setCheckable(true);
connect(theFullscreenAction, SIGNAL(toggled(bool)), this, SLOT(toggleFullscreen(bool)));
addAction(theFullscreenAction);
ui.videoFrame->addAction(theFullscreenAction);
void myApp::toggleFullscreen(bool bFullscreen)
	{
	if (bFullscreen)
		{
		videoFrameParent = ui.videoFrame->parentWidget();
		videoFrameParentLayout = ui.videoFrame->parentWidget()->layout();
		videoFrameFlags = ui.videoFrame->windowFlags();
		videoFrameSize = ui.videoFrame->size();

		// untie it...
		ui.videoFrame->setParent(NULL);
		ui.videoFrame->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);

		// hack for showing it on a screen, where the app is (in a multimonitor setup)
		QDesktopWidget *dw = QApplication::desktop();
		QRect drect = dw->screenGeometry(dw->screenNumber(QCursor::pos()));
		int desk_x = drect.width();
		int desk_y = drect.height();
		int x = ui.videoFrame->width();
		int y = ui.videoFrame->height();
		ui.videoFrame->move(desk_x / 2 - x / 2 + drect.left(), desk_y / 2 - y / 2 + drect.top());

		// yeah!
		ui.videoFrame->showMaximized();
		}
	else
		{
		// bring it back!
		ui.videoFrame->resize(videoFrameSize);
		ui.videoFrame->overrideWindowFlags(videoFrameFlags);
		ui.videoFrame->setParent(videoFrameParent);
		videoFrameParentLayout->addWidget(ui.videoFrame);
		ui.videoFrame->show();
		}
	}

Miłego oglądania! 😉

Facebooktwittergoogle_plusredditpinterestlinkedinmail
twitterlinkedin