NetTalk-Plugins schreiben in C++

Ein Tutorial

Dieses Tutorial soll zeigen wie man ein Plugin in C++ schreib, welches auf in einem Channel gesprochene Worte reagiert.

Schritt 1: WinMain

Zuerst (wie sollte es anders sein) brauchen wir ein Windows-Programm-Grundgerüst:

// LaberPlugin.cpp
//
// Zuerst ein paar includes
#include <windows.h>
#include <vector>
#include <string>
 
using namespace std;
 
 
// Dann ein paar Variablen (was wären wir nur ohne)
char szClassName[]="netteFensterklasse"; // Fensterklasse
string strPluginName;
char szNumberBuffer[33]; // Um Zahlen in Text umzuwandeln
 
HWND hPluginHwnd;     // Handle unseres Fensters
HWND hNTalkHwnd;      // Handles des NetTalk-Kommunikationsfensters
string strPluginHwnd; // string unseres Plugin-Handles
string strNTalkHwnd;  // string des NetTalk-Handles
 
 
// Hier noch ein paar Prototypen
LRESULT CALLBACK WinProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
BOOL StartPlugin(char *cmdLine);
LRESULT SendPlain(string text);
LRESULT SendPlain(char *text);
LRESULT SendText(string text, int FrameId);
LRESULT SendText(char *text, int FrameId);
LRESULT ExecuteNTalkCommand(char *command);
void tokenizeString(vector<string> &tokenList, char *plainStr, char seperator);
LRESULT Chan_Msg(string Text, string Nick, string Channel, int FrameId, int ConnId);
 
 
// Hier kommt endlich die WinMain
int WINAPI WinMain (HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdLine, int cmdShow) {
	HWND hwnd;
	MSG msg;
	WNDCLASSEX wc;
 
	// Fensterklasse definieren
  wc.cbSize=sizeof (WNDCLASSEX);
  wc.cbClsExtra=0; wc.cbWndExtra=0;
  wc.hInstance=hInst; wc.lpszClassName=szClassName;
  wc.lpfnWndProc=WinProc; wc.hIcon=LoadIcon (NULL, IDI_APPLICATION);
  wc.hIconSm=LoadIcon(NULL, IDI_APPLICATION);
  wc.hCursor=LoadCursor(NULL, IDC_ARROW);
  wc.style=CS_DBLCLKS; wc.hbrBackground=(HBRUSH) COLOR_BACKGROUND;
  wc.lpszMenuName=NULL;
 
	// Fensterklasse registrieren
	if (!RegisterClassEx (&wc)) {
		MessageBox(NULL, "Failed to register Window!", "Error", MB_OK | MB_ICONEXCLAMATION);
		return 0;
	}
 
	// und endlich...ein Fenster erstellen
	hwnd=CreateWindowEx (
		0,
		szClassName, "KAnJu - Pre-Beta-App",
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT,
		544, 375,
		HWND_DESKTOP,
		NULL, hInst, NULL
	);
 
	if(hwnd==NULL){
		MessageBox(NULL, "Failed to create window!", "Error", MB_OK | MB_ICONEXCLAMATION);
		return 0;
	}
 
  // erstmal unser eigenes Handles merken (in Zahl und Text)
	hPluginHwnd=hwnd;
	strPluginHwnd=string( ltoa( hPluginHwnd, szNumberBuffer, 10 ) ); 
 
	//ShowWindow (hwnd, cmdShow); // Nein, das Fenster wollen wir nicht zeigen ;p
 
	// Das Plugin starten (Wenn das nicht klappt wird das Programm beendet)
	if(!StartPlugin(cmdLine)) {
		DestroyWindow(hwnd);
		return -1;
	}
 
	// Und der ach so beliebte Message-Loop
	while (GetMessage (&msg, NULL, 0, 0)) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}// end while(GetMessage())
 
	return msg.wParam;
}// end #WinMain()

Anmerkung: Beim Plugin-Namen ist unbedingt(!) darauf zu achten, das dieser keine Leerzeichen enthält.

Schritt 2: StartPlugin

Jetzt kommt die Funktion 'StartPlugin(char *)'. Hier müssen wir uns bei NetTalk 'anmelden'. Den Parameter (die Kommandozeile welche der WinMain übergeben wurde) brauchen wir um den Handle des NetTalk-Kommunikationsfensters zu erhalten (diesen erhält man auch wen man in NetTalk den Befehl '?DataPort' in der Textleiste eingibt). Läuft hier alles so ab wie gedacht, wird TRUE zurück gegeben, sonst FALSE.

BOOL StartPlugin(char *cmdLine) {
	// Parameter parsen
	vector<string> parameter;
	tokenizeString(parameter, cmdLine, ' ');
 
	if(parameter.size()==0) return FALSE; // es wurden keine Parameter übergeben!
 
  // der String des NetTalk-Fensterhandles ist der letzte (oder einzige) Parameter.
	strNTalkHwnd=parameter[parameter.size()-1];
  // Da uns der String aber nicht weiter hilft, müssen wir ihn in eine Zahl umwandeln
	hNTalkHwnd=(HWND)atol(parameter[parameter.size()-1].c_str());
 
	if( IsWindow(hNTalkHwnd)==0 ) return FALSE; // Parameter beschreibt kein gültiges Fenster!
 
 
	// Plugin bei NetTalk anmelden. "<PluginName> inst <PluginHwnd>"
	str=strPluginName + string(" inst ") + strPluginHwnd;
	SendPlain(str);
 
	// Jetzt sagen wir NetTalk, auf was für Ereignisse das Plugin reagieren soll.
  // "<PluginName> addevent <ScriptEvent>"
	str=strPluginName + string(" addevent Chan_Msg");
	SendPlain(str);
 
	return TRUE;
}// end #StartPlugin()

Schritt 3: Send...

Jetzt will ich mal zu den 'Send…()' Befehlen kommen. Mit deren Hilfe senden Wir Botschaften an NetTalk. 'SendPlain()' Sendet den übergebenen Text dabei so wie er ist an NetTalk. 'SendText()' hingegen löst durch den „send“ Befehl die Scriptfunktion 'PluginEvent()' aus, an welche dann der Parameter 'text' gesendet wird (“<PluginName> send <Text>“). Die Funktion 'SendText()' schreibt zusätzlich auch noch die FrameId (und ein Komma) vor den Text, damit das Script später auch weiß wo es den Text hinsenden soll.

LRESULT SendPlain(char *text)
{
	return SendMessage(hNTalkHwnd, WM_SETTEXT, (WPARAM)(-1), (LPARAM)text);
}// end #SendPlain()
 
LRESULT SendPlain(string text){
	return SendPlain((char *)text.c_str());
}// end #SendPlain()
 
LRESULT SendText(char *text, int FrameId)
{
	return SendText( string(text) , FrameId);
}// end #SendText()
 
LRESULT SendText(string text, int FrameId)
{
	char numbuffer[11];
	string str;
	memset(numbuffer, 0, sizeof(numbuffer));
	str=strPluginName;
	str+=" send ";
	str+=string( ltoa(FrameId, numbuffer, 10) );
	str+=string( "," );
	str+=text;
	return SendMessage(hNTalkHwnd, WM_SETTEXT, (WPARAM)(-1), (LPARAM)str.c_str());
}// end #SendText()

Schritt 4: WinProc

Puh, so weit so gut. Jetzt fehlt natürlich noch die FensterFunktion für die ganzen Nachrichten die während eines Pluginlebens so eintrudeln. An Dieser Stelle sei auch noch etwas angemerkt: Wenn man im NetTalk-Einstellungsfenster bei einem Plugin auf 'Bearbeiten' klickt, wird die Botschaft 'show' an das ausgewählte Plugin gesendet. Damit hat es dann die Möglichkeit ein Fenster mit Optionen zum Script anzuzeigen. Da ein 'schriftliches' Fensterdesign jedoch viel Platz in anspruch nimmt (und unser kleines Plugin keine Optionen braucht) lasse ich es ganz einfach mal weg :D .

LRESULT CALLBACK WinProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
	switch (message) {
		case WM_CREATE:
			// Wer einen Optionsdialog wünscht, kann das aktuele Fenster dazu
			// verwenden un an dieser Stelle seine Controls einfügen.
			return 0;
		case WM_SETTEXT:
			// WM_SETTEXT. Über diese Nachricht sendet uns NetTalk seine
			// Befehle (string im 'lParam'). Um diese Funktion klein zu hal-
			// ten, leiten wir diesen Parameter einfach weiter:
			ExecuteNTalkCommand((char *)lParam);
			return -1; // <- Wichtig
		case WM_CLOSE:
			// Wenn beim Optiondialog auf's Kreuz geklickt wird, soll das
			// Fenster nur unsichtbar werden(und nicht das Programm beenden).
			ShowWindow(hwnd, SW_HIDE);
			return 0;
		case WM_DESTROY:
			PostQuitMessage(0); // Ok, Finito. Alles hat mal ein Ende ;)
			return 0;
		default:
			return DefWindowProc (hwnd, message, wParam, lParam);
	}// end switch(message)
 
	return 0;
}// ebd #WndProc()

Schritt 5: ExecuteNTalkCommand

Jetzt? Nein, wir können es immer noch nicht testen … aber bald :). Wie der Aufmerksame Leser bemerkt hat fehlt immernoch einige Funktionen. 'ExecuteNTalkCommand(char*)' sei als nächstes an der Reihe. Diese Funktion wertet den von NetTalk übergebenen String aus und ruft dann die entsprechenden (eigenen) Funktionen auf.

LRESULT ExecuteNTalkCommand(char *command){
	if(command==NULL) return -1; // <-- für den Fall der Fälle
 
	// Kommando finden
	vector<string> parameter;
	string strCommand;
	char *endPointer;
	endPointer=strchr(command,' ');
	if(endPointer==NULL) {
		// Nur Kommando, keine Parameter
		strCommand=string(command);
	}else{
		// Kommando UND Parameter
		*endPointer=0;
		strCommand=string( command );
		*endPointer=' ';
		endPointer++;
		command=endPointer;
		// Parameter finden
		tokenizeString(parameter, command, (char)0x1b);
	}
 
	// Kommandos auswerten
	if(strCommand=="unload"){
		// Plugin soll beendet werden
		SendPlain("LaberPlugin unload");
		DestroyWindow(hPluginHwnd);
		return -1;
	}
	if(strCommand=="show"){
		// Plugin soll seinen Optionsdialog anzeigen
		//ShowWindow(hPluginHwnd,SW_SHOW); // <- nur nötig wenn es Optionen gibt
		return -1;
	}
	if(strCommand=="chan_msg"){
		// Irgendwer hat in irgendeinem Channel was gesagt
		if(parameter.size()!=5) return -1;
		return Chan_Msg(parameter[0],parameter[1],parameter[2],atoi(parameter[3].c_str()),atoi(parameter[4].c_str()));
	}
 
	return -1;
}// end #ExecuteNTalkCommand()

Schritt 6: tokenizeString

Ja genau. Jetzt kommt die Funktion, welche die einzelnen Parameter aufsplittet. Sie erkennt Worte in der Zeichenkette 'plainStr', welche durch das Zeichen 'seperator' getrennt sind. Jedes gefundene Wort wird dem Voktor 'tokenList' hinzugefügt

void tokenizeString(vector<string> &tokenList, char *plainStr, char seperator){
	int len;
	char *start, *end;
	len=strlen(plainStr);
	start=plainStr;
	while(1){
		end=strchr(start,seperator);
		if(end==NULL) break;
		if(end==start){
			start++;
			continue;
		}
		*end=0;
		tokenList.push_back(std::string(start));
		*end=seperator;
		start=end;
	}
	if(*start!=0) tokenList.push_back(std::string(start));
}// end #tokenizeString()

Schritt 7: Chan_Msg

So, jetzt kommen wir endlich zu 'Chan_Msg()'. Da wir uns bis hier her so viel Mühe gemacht haben, brauchen wir uns hier kaum noch um etwas zu kümmern, nur darum was denn geschehen soll wenn einer was im Channel sagt. Da es aber ziemlich nervend wäre wenn das Plugin auf jeden Text reagieren würde, soll es in diesem Beispiel nur reagieren wenn ein Bestimmter Nick etwas sagt (Olli935).

LRESULT Chan_Msg(string Text, string Nick, string Channel, int FrameId, int ConnId){
	// Wörter/Parameter parsen
 
	if(Nick=="Olli935"){
		SendText("Schaut mal alle her! Olli hat was gesagt!", FrameId);
		return -1;
	}
 
	return -1;
}// end #Chan_Msg()

Schritt 8: Das Script

An dieser Stelle kann man es schon kompilieren uns ins NetTalk-Verzeichnis kopieren. Denkt aber dran der Datei die Endung '.plg' zu geben (Aus „LaberPlugin.exe“ wird „LaberPlugin.plg“)! Alles was jetzt noch fehlt ist ein Script. Dazu einfach im NetTalk-Menü auf 'Script/Bearbeiten' gehen und folgendes eintippen. Hier gilt zu beachten das ihr den Namen eures Plugins (LaberPlugin) vollständig in Kleinbuchstaben eingeben müsst (laberplugin).

Sub PluginEvent(Plugin, EventId, Data)
  if Plugin = "laberplugin" AND EventId = 3 then
    pos=Instr(1,data,",")
    if pos>0 then
      FrameId=left(data,pos-1)
      Data=mid(data,pos+1)
      Send Data, FrameId
    end if
  end if
end sub

Fertig. Das ganze macht zwar nicht viel (wohl eher wenig) aber ich denke das ihr jetzt in der Lage sein solltet eigene Plugins für NetTalk zu schreiben. Ebenso müsstet ihr nun auch mit anderen Events arbeitet können, geht dabei einfach genauso vor wie es hier für 'Chan_Msg' gemacht wurde … oder macht es ganz anders wenn es euch nicht gefällt ;). Viel Spaß!


Eigene Werkzeuge