previous next Title Contents

7. Kommunikointi ohjelmien välillä.


Kari Heikkilä (DDE-teoria) & VL

Windowsissa ohjelmat voivat vaihtaa tietoa seuraavilla eri tavoilla

* tiedostojen välityksellä

* leikekirjan välityksellä

* DDE-linkin avulla

* OLE-linkin avulla

* ikkunoille lähetettävien viestien välityksellä

* Drag and drop -menetelmällä (edellisen sovellus)

* jaetun globaalin muistin kautta (lopulta DDE yms. johtavat tähän)

* suoraan lukemalla tietoa toisen ikkunasta (harvinaista)

Tallettamalla oman ohjelman tiedot aina tekstimuodossa tai ainakin lisäämällä ohjelmaan valinta, jolla voidaan lukea ja kirjoittaa tekstimuotoista tietoa, saadaan jonkinlainen tiedostojen välityksellä tapahtuva yhteys lähes mihin tahansa ohjelmaan.

Muista siirtotavoista ainakin yksinkertainen leikekirjamuoto ja miksei yksinkertainen DDE-linkkikin pitäisi sisällyttää jokaiseen hyötykäyttöön tulevaan ohjelmaan. Koska DDE- ja OLE- ovat hyvin monipuolisia välitysmekanismeja, on niiden ohjelmointi myös varsin työlästä. Tämän luvun lopussa esitellään kaksi aliohjelmakirjastoa joilla yksinkertaiset DDE-tapaukset voidaan ohjelmoida muutamalla rivillä.

7.1 DDE:n Perusteita

DDE (Dynamic Data Exchange) on eräs muoto välittää tietoa prosessien kesken Windows-ohjelmissa. DDE on itseasiassa jaetun muistin käyttöä, jota varten Windows API:ssa on lukuisa joukko funktiota synkronoimaan tiedon välitystä.

Perusmuoto ohjelmien väliseen tiedon siirtoon on leikekirja (clipboard). Siinä tieto kopioidaan leikekirjaan ja siitä edelleen johonkin ohjelmaan. Ohjelmaan kopioitu tieto ei ole enää mitenkään sidoksissa alkuperäisen tiedon kanssa. DDE:tä voi myös käyttää tällaiseen kertaalleen tapahtuvaan siirtoon, mutta useimmiten DDE:tä käytetään "on-line" yhteyksissä, joissa tiedon muutos halutaan välittää automaattisesti johonkin toiseen ohjelmaan.

DDE on kuten muukin Windows-ohjelmointi viestien lähettämistä ja vastaanottamista. DDEML (Dynamic Data Exchange Management Library) tarjoaa ohjelmointiliittymän DDE-viestien välittämiseen. Vanhemmat Windows-ohjelmat käyttävät suoraan DDE API-kutsuja, mutta ovat kuitenkin yhteensopivia DDEML:n kanssa. DDEML on käytettävissä Windows 3.0:sta alkaen, ei kuitenkaan tue Windows 3.0:n real-tilaa.

7.1.1 Asiakas ja palvelin

DDE tapahtuu aina asiakas- ja palvelinsovelluksen kesken. Asiakas alustaa aina ensin DDE-yhteyden, jonka jälkeen voi lähettää transaktioita palvelimella (transaktio tarkoittaa tässä pyyntöä tietoon tai palveluun). Palvelin vastaa transaktioon tarjoamalla pyydettyä palvelua tai kieltäytymällä siitä (jos palvelua ei ole tai palvelimella on muuten kiireistä, eikä se pysty tarjoamaan asiakkaalle palvelua). Palvelin on se DDE-ohjelma, joka "lähempänä" tietoa tai tarjottavaa palvelua.

Asiakasohjelma voi olla samanaikaisesti yhteydessä useaan eri palvelimeen (jopa useasti samaan) ja palvelimella voi olla yhtäaikaa useita asiakkaita. Sama ohjelma voi olla sekä asiakas että palvelin.

7.1.2 Transaktiot ja DDE-funktio

DDEML ilmoittaa DDE-tapahtumasta lähettämällä transaktion ohjelman DDE-funktiolle (DDE callback function). Transaktio on vastaavanlainen tunnus (vakio) kuin ikkuna-funktion viesti. DDE-funktion täytyy vastata transaktioihin kunkin transaktion vaatimalla tavalla.

Esimerkiksi asiakasohjelma alustaa keskustelun (conversation) kutsumalla DdeConnect-funktiota. Kutsusta seuraa, että DDEML lähettää XTYP_CONNECT -transaktion palvelinohjelman DDE-funktiolle. DDE-funktio voi sallia keskustelun palauttamalla TRUE:n DDEML:lle tai kieltäytymällä keskustelusta palauttamalla FALSE:n. Edelleen DDEML lähettää asiakkaalle transaktion XTYP_CONNECT_CONFIRM, josta asiakas tietää yhteyden muodostuneen.

7.1.3 Palvelun nimeäminen

DDE-palvelin käyttää kolmitasoista nimeämishierarkkiaa: palvelu (service name, vanhemmissa dokumenteissa application name), aihe (topic name) ja asia (item name). Nämä yhdessä määräävät yksikäsitteisesti toimenpiteen keskustelun aikana.

palvelu
service

merkkijono, joka asiakkaan täytyy tuntea alustaessaan keskustelun. Tämän tunnuksen DDE-palvelin yleensä rekisteröi käynnistyessään ja DDE-asiakkaisiin on mahdollista tehdä ominaisuus, jolla käynnissä olevia DDE-palvelimia voidaan tiedustella.  Tämä on yleensä sama kuin ohjelman nimi.

aihe
topic

jakaa palvelun loogisiin kokonaisuuksiin. Esimerkkinä olevassa autolaskuriohjelmassa (palvelin) aihe on "Lkm" eli tästä palvelun osasta saadaan selville lukumääriä. Toinen aihe voisi olla "Asetukset", jossa voitaisiin vaikkapa määrätä lisää laskettavia asioita tai määrätä palvelin nollaamaan laskurit.

asia
item

määrää lopulta kohteen, jota transaktio koskee. Asia voi määrätä esimerkiksi mitä kokonaislukua, merkkijonoa, tekstiä tai bittikarttaa transaktio koskee.

Esimerkiksi jos Excelissä pyydetään DDE-palvelua autolaskurin henkilöautojen lukumäärän selvittämiseksi, voisi Excelissä olla kaava:


7.2 DDE ohjelmointi

7.2.1 DDE:n alustaminen

Ensimmäisenä DDEML:n funktioista täytyy kutsua DdeInitialize-funktiota Tämä rekisteröi mm. ohjelman DDE-funktion ja transaktio suotimet (transaction filter) DDEML:lle. Ennen tätä funktiota täytyy DDE-funktiosta luoda esiintymä (instance) MakeProcInstance-funktiolla.

	DWORD idInst = 0L;		/* DDE-esiintymän tunnus			*/
	HANDLE hInst;		/* koko ohjelman esiintymän tunnus 		*/
	FARPROC lpDdeProc;		/* DDE-funktion esiintymän osoite		*/
	
	...
	
	lpDdeProc = MakeProcInstance ( (FARPROC) DDECallback, hInst );
	if ( DdeInitialize(&idInst, (PFNCALLBACK)lpDdeProc, 
				 APPCLASS_STANDARD | CBF_FAIL_EXECUTES | CBF_FAIL_POKES,
				 0L ) ) {
	     MyError( "DDE:n alustus ei onnistu!" );
	     return FALSE;
	}

7.2.2 DDE-funktio

Jokaisella ohjelmalla joka käyttää DDEML:ää täytyy olla DDE-funktio (DDE Callback Function), joka ottaa vastaan ja suorittaa DDEML:n lähettämät tehtävät. Mitä transaktioita DDEML lähettää DDE-funktiolle riippuu DdeInitialize kutsussa asetetuista suotimista. Koko järjestelmän tehokkuutta voidaan parantaa suodattamalla pois kaikki transaktiot, joihin ei voida järkevästi vastata. Esimerkki DDE-palvelin vain lähettää tietoja asiakkaalle, jolloin alustuksessa pyydetään suodattamaan pois XTYP_EXECUTE ja XTYP_POKE -transaktiot. Transaktiotyypeistä myöhemmin.

	/***************************************************************************/
	#pragma argsused
	HDDEDATA EXPENTRY DDECallback (                                       /* d */
	   WORD wType,     /* transaktion tyyppi                                   */
	   WORD wFmt,      /* datan tyyppi (clipboard type)                        */
	   HCONV hConv,    /* keskustelukahva                                      */
	   HSZ hsz1,       /* merkkijonokahva, kts. ko. transaktiosta              */
	   HSZ hsz2,       /* "                                                    */
	   HDDEDATA hData, /* kahva globaaliin muistilohkoon                       */
	   DWORD dwData1,  /* transaktiokohtaista dataa                            */
	   DWORD dwData2 ) /* "                                                    */
	{
	  int i;
	  char szBuffer[10];
	
	  switch ( wType )   {
	
	    case XTYP_CONNECT:    // Asiakas haluaa avata keskustella
	                                        // hsz1 = topic
	                                        // hsz2 = service 
	      if ( hsz2 == hszService )
	        return TRUE; // Meillä on sopiva keskusteluaihe, ok
	      else
	        return FALSE; // Ei sopivaa aihetta
	
	      case XTYP_ADVSTART:   // Asiakas haluaa tietää jokaisesta muutoksesta
	      case XTYP_ADVSTOP:    // Asiakas ei enää halua tietää ...
	                                        // hsz1 = topic
	                                        // hsz2 = item
	
	      // Tarkistetaan, että pyydetty laskuri löytyy                                  
	      if ( MikaLaskuri( wFmt, hsz2 ) < 0 )
	        return FALSE;  // Ei löydy
	
	      // Jos laskuri löytyi, palautetaan asiakkaalle myönteinen vastaus
	      return TRUE;
	
	    case XTYP_REQUEST:    // Asiakas pyytänyt dataa
	    case XTYP_ADVREQ:     // Data muuttunut palvelimella
	                                        // hsz1 = topic
	                                        // hsz2 = item
	      if ( (i= MikaLaskuri(wFmt,hsz2) ) < 0 )
	        return NULL;  // Ei löydy laskuria
	
	      /* muutetaan laskurin arvo merkkijonoksi  */
	      wsprintf(szBuffer,"%5d.",laskurit[i]);
	
	      return DdeCreateDataHandle(idInst,&szBuffer,sizeof(szBuffer)+1, 
	                                 0L,hsz2,wFmt,0);
	
	
	    case XTYP_CONNECT_CONFIRM:  // Asiakas hyväksynyt keskusteluyhteyden
	                                        // hsz1 = topic
	                                        // hsz2 = service 
	      hConvApp = hConv;
	      break;
	
	    case XTYP_DISCONNECT:  // Asiakas halusi lopettaa keskustelun
	                                        // hsz1 = topic
	                                        // hsz2 = service 
	      MyInfo( "DDE asiakas sulki yhteyden." );
	      hConvApp = NULL;
	      break;
	
	    case XTYP_ERROR: // Jokin virhe
	      break;
	  }
	   
	  return NULL;
	}

DDE-funktion wType-parametri kertoo transaktion tyypin ja sitä seuraavat parametrit ovat transaktiotyyppikohtaisia. Esimerkkiin on merkitty parametrien hsz1 ja hsz2 merkitykset.

7.2.3 Merkkijonojen hallinta

Useiden DDEML-funktioiden täytyy välittää merkkijonoja transaktion aikana. Esimerkiksi asiakkaan täytyy välittää palvelun nimi ja aihe luodessaan yhteyttä DDE-palvelimeen DdeConnect-funktiolla. Tällaisten merkkijonojen välitykseen käytetään merkkijonokahvoja (string handle).

Merkkijonokahva luodaan kutsumalla DdeCreateStringHandle-funktiota. Funktio rekisteröi merkkijonon Windows-systeemiin. Seuraavassa esimerkissä luodaan kahva palvelun nimeä varten:

	   HSZ hszServName;
	...
	   hszService = DdeCreateStringHandle ( 
				 idInst,	// DDE esiintymän tunnus
				 "AUTOLDDE",	// rekisteröitävä merkkijono
				 CP_WINANSI );	// koodisivu merkkijonon käännöstä varten

idInst-parametri on jälleen peräisin DdeInitialize-funktion kutsusta. Viimeinen parametri kertoo mitä Windowsin koodisivua käytetään merkkijonon kääntämiseen. Windowsissa paraikaa käytössä oleva koodisivun tunnus saadaan GetKBCodePage-funktiolla.

Tällä tavoin luotu merkkijono sijoittuu systeemin globaaliin muistiin. Kun merkkijono välitetään kahvana DDEML:n kautta, merkkijono on kopioitava globaalista muistista lokaaliin muistiin ennenkuin se on normaalisti käsiteltävissä C:n merkkijonona. Esimerkkiohjelman MikaLaskuri-funktiossa merkkijono haetaan kahvasta lokaaliin merkkitaulukkoon ja käytetään tätä jonoa tutkittaessa löytyykö haluttua tietoa.

	int MikaLaskuri( HSZ hszItem )  {
	
	  int i;
	  char szItem[10];
	
	  // Tarkistetaan, että on pyydetty tekstimuotoista dataa
	  if ( wFmt != CF_TEXT )
	    return -1;
	
	  DdeQueryString( idInst, 	// DDE:n esiintymän tunnus
	                  hszItem,	// kahva mistä kopioidaan
				szItem, 	// osoitin mihin kopioidaan
				sizeof(szItem),// maksimissaan kopioitava määrä
				0);		// koodisivu  

Esimerkissä kuitenkin se vika, että tunnus ei voi olla vakiopituutta suurempi ja merkkejä voi jäädä lukematta. Parempi tapa olisi vaikkapa seuraavan näköinen:

	DWORD	idInst;
	DWORD	nLen;
	HSZ		hszServ;
	PSTR	pszServName;
	...
	nLen = DdeQueryString( idInst, hszServm (LPSTR) NULL, 0L, CP_WINANSI ) + 1;
	pszServName = (PSTR) LocalAlloc( LPTR, (WORD) nLen );
	...
	LocalFree( (HLOCAL) pszServName );

Jos merkkijonokahva halutaan säilyttää myöhempiä DDE-funktion kutsukertoja varten, täytyy kutsua

	DdeKeepStringHandle(hInst,hsz)

-funktiota. Ilman kutsua merkkijonoon ei päästä myöhemmin käsiksi.

Kun ohjelma kutsuu DdeCreateStringHandle -funktiota, systeemi luo merkkijonon koko systeemin kattavaan merkkijonotauluun ja kahvan, jolla merkkijonoon päästää käsiksi. Kun luodaan kahva merkkijonoon, joka on jo olemassa, ei uutta merkkijonoa luoda, vaan kasvatetaan olemassaolevan merkkijonokahvan luontikertoja ( DdeKeepStringHandle myös kasvattaa luontikertoja). Lukuarvoa vastaavasti pienennetään DdeFreeStringHandle-kutsulla. Kun luontikertojen määrä menee nollaan, merkkijono hävitetään. Tästä syystä ohjelmissa on oltava tarkkana ettei luontikertoja vähennetä enempää kuin niitä lisätty, koska jollakin muulla DDE-ohjelmalla voi olla sama merkkijonokahva käytössä.

DDEML:n merkkijonojen hallinta perustuu Windowsin atomi systeemiin, joten DDE-merkkijonoilla on vastaavat kokorajoitukset.

7.2.4 Nimipalvelu

DDEML mahdollistaa DDE-palvelimelle tarjoamiensa palvelujen rekisteröinnin ja estää DDEML:ää lähettämästä muun nimisten XTYP_CONNECT transaktioiden lähettämisen DDE-funktiolle (tämä on oletusarvo).

Jos halutaan sallia kaikkien XTYP_CONNECT-transaktioiden tulo DDE-funktiolle, lisätään lippu DNS_FILTEROFF DdeNameService-kutsuun (pätkä MyInitDDE-funktiosta):

	   // Rekisteröidään palvelu
	   DdeNameService ( 
			idInst, 
			hszService, 			  // merkkijonokahva palvelun nimeen
			(HSZ) NULL, 			  // varattu 
			DNS_REGISTER | DNS_FILTEROFF ); // liput

DDE-palvelimen tulee rekisteröidä palvelunsa mahdollisimman pian DdeInitialize-kutsun jälkeen ja poistaa palvelun nimi rekisteristä juuri ennen DdeUninitialize-kutsua.

	 // Poistetaan palvelu rekisteristä
	 DdeNameService ( idInst, hszService, (HSZ) NULL, DNS_UNREGISTER );
	 DdeUnInitialize( idInst );

7.2.5 Tiedon välittäminen

Kaikki muu tieto joka ei välity DDE-funktion parametreina täytyy välittää globaalien muistilohkojen kautta. Kahva muistilohkoon luodaan DdeCreateDataHandle-funktiolla. DDE-funktio palauttaa tämän kahvan DDEML:lle, joka edelleen välittää kahvan tietoa kutsuneelle ohjelmalle. Oletuksena muistikahva ei säily DDE-funktiosta palattaessa, joten kahvan vapauttamisesta ei tarvitse huolehtia. Kahva voidaan kuitenkin luoda siten, että se jää luoneen ohjelman hallintaan. Tämä saattaa olla tarpeen palvelussa jossa samaa dataa välitetään monelle asiakkaalle. Tällöin täytyy tietenkin ohjelman osata vapauttaa kahva ohjelman loppuessa.

Esimerkeissä tieto autolaskurin arvoista on välitetty taulukkolaskentaohjelmille. Asiakas voisi lukea laskurin arvon seuraavasti:

	HDDEDATA 	hData;		  	// DDE-datakahva
	LPBYTE 	lpszGlobalData;  	// muistikahva
	DWORD 	cbDataLen;	  	// saadun datan määrä
	HSZ		hszLkmHA;	  	// merkkijonokahva HA-tunnukselle
	HCONV	hConv;		  	// keskustelukahva
	char 	szLocalData[80]; 	// lokaali puskuri datalle
	...
	
		hData = DdeClientTransaction(
	    		(LPBYTE) NULL, // ei dataa palvelimelle
	    		0,             // datan pituus (palvelimen suuntaan)
	    		hConv,         
	    		hszLkmHa,
	    		CF_TEXT,       // tiedon tyyppi
	    		XTYP_REQUEST,  // pyydetään data kertaalleen
	    		10000,         // odotetaan enintään 10 sekuntia
	    		NULL );    	// ei olla kiinnostuneita mahdollisen
						// virheen syystä
	
		if ( hData != NULL ) {
	  	  lpszGlobalData = DdeAccessdata( hData, &cbdataLen );
		  strncpy( szLocalData, lpszGlobaldata, sizeof(szLocalData) );
	 	  DdeGetData(
			hData,		// kahva globaaliin muistilohkoon (mistä kopioidaan)
			szLocalData,	// osoite johon kopioidaan
			cbDataLen,	// mksimimäärä mitä kopioidaan
			0 );		// offset mihin kopioidaan
		  DdeUnaccessData( hData );
		}
	
		/* tehdään jotakin saadulle lukuarvolle */
	...

7.3 Tiedon välittämisen tyylit ja tietoa pyytävät transaktiot

Tiedon välitys voidaan jakaa asiakkaan ja palvelimen välillä kolmeen tyyppiin: manuaalinen, automaattinen ja ilmoituslinkki (manual, automatic, notify; vanhat nimet kylmä, kuuma ja lämmin linkki). Nämä perusperiaatteet on syytä tuntea, koska liittyvät läheisesti transaktiotyyppeihin.

7.3.1 Manuaalinen linkki (manual link, kylmä linkki)

Tässä tyypissä asiakas pyytää tietoa palvelimelta silloin kun se sitä tarvitsee. Asiakas ei tällöin ole tietoinen palvelimella olevan tiedon muuttumisesta. Asiakas lähettää XTYP_REQUEST-transaktion palvelimelle ja saa paluuarvona kahvan palvelimen lähettämään dataan tai NULL:n, jos palvelin ei jostain syystä pysty tarjoamaan kysyttyä palvelua.

7.3.2 Automaattinen linkki (automatic link, kuuma linkki)

Tässä keskustelutyypissä asiakas pyytää palvelinta lähettämään datan jokaisesta tiedon muutoksesta. Asiakas pyytää tämän transaktion lähettämällä XTYP_ADVSTART:n palvelimelle. Kun asiakas ei enää halua dataa muutoksista, se lähettää palvelimelle XTYP_ADVSTOP-transaktion. Tämä keskustelumuoto kuluttaa paljon systeemin resursseja.

7.3.3 Ilmoituslinkki (notify link, lämmin linkki)

Tämä on välimuoto manuaalisesta ja automaattisesta linkistä. Tässä tapauksessa palvelin lähettää tiedon datan muutoksesta asiakkaalle mutta ei varsinaista muuttunutta dataa. Jos asiakas haluaa saada muuttuneen datan käyttöönsä, lähetetään pyyntö kuten kylmän linkin tapauksessa. Ilmoituslinkki muodostetaan lähettämällä XTYP_ADVSTART | XTYPF_NODATA -transaktio palvelimelle.

7.3.4 Muut transaktiot

XTYP_POKE-transaktiota käytetään asiakkaan välitettäessä tietoa palvelimelle. Jos palvelin hyväksyy tiedon tyypin ja varsinaisen tiedon, se vastaa asiakkaalle DDE_FACK-tunnuksella.

Komentoja lähetetään palvelimelle XTYP_EXECUTE-transaktiolla.

7.3.5 Synkroninen ja asynkroninen tiedonsiirto

Synkronisessa tiedon siirrossa asiakas välittää DdeClientTransaction-kutsussa tiedon kuinka kauan transaktiota päättymistä odotetaan. Funktio ei palaa ennenkuin palvelin suorittanut tehtävän, transaktio ei muuten onnistui tai aika täytyy. Kutsussa olleesta lippumuuttujasta voidaan tutkia mikä meni vikaan.

Asynkronisessa siirrossa DdeClientTransaction-funktion palaa heti kun transaktio on siirretty DDEML:lle. Kun palvelin on suorittanut transaktion, DDEML lähettää asiakkaalle XTYP_XACT_COMPLETE-transaktion, jossa on mukana sama tunnus, jonka DdeClientTransaction palautti lähetettyään tehtävän DDEML:lle. Näitä tunnuksia tutkimalla asiakas voi päätellä, minkä tehtävän palvelin sai suoritettua.

7.3.6 Malli tiedonsiirrosta (DDE\MORFCLNT.C ja ALI\MORFO.C)

Morfo on Kielikone OY:n Sitran rahoituksella alunperin tekemä suomenkielen oikeinkirjoituksen tarkistus. Sen voi ostaa lisäosana vaikkapa Windows Wordiin. Koska Morfo toimii DDE-palvelimena, ei se tietenkään ole sidottu vain yhden ohjelman käyttöön. Mallissa on tehty edit-ikkuna, johon kirjoitetun sanan oikeellisuus voidaan tarkistaa Morfolla.

Käyttö vaatii tietysti DDEMORFO.EXE:n olemassa olon, mutta toiminnan ajatuksen voi selvittää hyvin asiakasaliohjelmasta MORFO.C.

Tosin Morfon tätä monistetta kirjoitettaessa käytössä ollut versio käytti DDE-palveluita varsin ahnaasti ja pudotti koneen suorituskyvyn puoleen silloin kun Morfo oli ladattuna.

Siis tutki ohjelmasi kuluttamat resurssit!

7.4 Yksinkertaistettu automaattinen linkki

Koska yksinkertaisenkin ja monesti hyvin tavallisen automaattisen linkin muodostamiseksi tarvitsee jopa DDEML:ääkin käyttäen kirjoittaa varsin paljon koodia, on seuraavassa kirjoitettu käyttöä helpottavat aliohjelmakirjastot sekä palvelimena toimimiseksi että asiakkaana toimimiseksi. Sama ohjelma voi tietysti toimia sekä palvelimena että asiakkaana.

Useat ohjelmat ymmärtävät automaattisen linkin merkkijonon avulla muodossa:

	service|topic!item

Näiden yksinkertaisten linkkien käsittelemiseksi on kirjoitettu kaksi kirjastoa DDESER ja DDECLI. Seuraavassa kummastakin kirjastosta on lyhyt kuvaus yksinkertaisimmasta käytöstä. Lisätietoa monipuolisemmasta käytöstä löytyy kummankin aliohjelmakirjaston .C -tiedoston kommenteista.

7.4.1 Palvelin (ALI\DDESER.C)

Jos esimerkiksi Exceliin haluttaisiin saada linkki autolaskurista, voitaisiin autolaskurista tehdä DDE-palvelin. Autolaskurin henkilöautojen lukumäärä saataisiin näkymään tietyssä Excelin solussa kaavalla:

	=AUTOLDDE|LKM!HA

Quattro Pro for Windowsissa vastaava kaava olisi:

	@DDELINK([AUTOLDDE|LKM]"HA")

Tavoitteena on saada ainakin tämä yksinkertainen tilanne helpoksi. Useinhan jonkin tietyn dialogin (tai ikkunan) Edit- tai Static-kentän tieto halutaan välittää toiselle ohjelmalle.

Ohjelmoijan kannalta tavoite voisi olla vaikkapa seuraava:

	1) (pää)ohjelmaan lisättävä alustukset ja lopetus:
	          InitDDEServer(hInstance,"AutolDDE",NULL);
	          AddAutoTextService("Lkm","ha",HAL,hWnd);
	          AddAutoTextService("Lkm","ka",KAL,hWnd);
	           ...
	          CloseDDEServer();
	
	2) Kun tieto muuttuu (vaikkapa aliohjelmassa ShowStatic) kutsutaan:
	          InformDDEChangeSN(nr);
	
	   jossa nr on muutoksen mukaan joko HAL tai KAL

Paljon tämän vähemmällä ohjelmoinnilla ei DDE-linkkiä voida saavuttaa. Kirjasto DDESER.C sisältää mm. edellä mainitut aliohjelmat. Jos palveltava data on monimutkaisempaa kuin pelkkä tietyn Edit- tai Static- kentän sisältö, voidaan linkin alustuksessa (AddService, tai InitDDEService) ilmoittaa funktio, joka käsittelee datan ja palauttaa sen.

Käyttäjän kannalta olisi mukavaa, jos linkkikenttiä vastaavaa kaavaa ei tarvitsisi asiakasohjelmassa (Excel tai Quattro tms.) kirjoittaa. Usein ohjelmien Edit-valikosta löytyy kohta Paste link, jolla leikekirjassa oleva linkkitietous muutetaan ko. ohjelman ymmärtämäksi linkiksi (taulukkolaskennan tapauksessa oikeaksi kaavaksi).

Leikekirjassa ei ole valmista linkkityyppiä, mutta nykyisin useat ohjelmat tunnistavat lisätyn tyypin "Link" (ks. ALI\CLIPBOARD.C). Voimme tehdä omaan ohjelmaan muutoksen, jolla leikekirjaan lisätään linkkityyppi kutsumalla jotakin DDESER-kirjaston kutsuista:

	LinkToClipboardSN(hWnd,HAL)
	LinkToClipboard(hWnd,"Lkm","ha");

Asiakasohjelmassa sitten otetaan tämä linkki leikekirjasta. Em. kutsut laittavat leikekirjaan linkin lisäksi myös itse kentän sisällön, jolloin kutsuja voidaan käyttää myös staattiseen tiedonsiirtoon.

7.4.2 Asiakas (ALI\DDECLI.C)

Omaan ohjelmaan voi olla turhauttavaa ohjelmoida jotakin monimutkaista matemaattista lauseketta tai regression laskua, jos taulukkolaskin osaa laskea ko. laskun valmiiksi. Tällöin oma ohjelma täytyy saada toimimaan taulukkolaskimen asiakkaana. Esimerkiksi asiakasyhteys Exceliin voitaisiin muodostaa DDECLI-kirjaston avulla seuraavasti.

Olkoon dialogissa hDlg Edit-ikkuna numerolla ID_TULOS varattuna Excelin soluun R5C7 laskemalle tulokselle. Excel käyttää esimerkiksi taulukkoa LASKU.XLS. Ohjelmoija lisää ohjelmaansa kutsun:

	AutoLinkFromText(0,hDlg,ID_TULOS,"Excel|LASKU.XLS!R5C7");

Ensimmäinen kokonaisluku on ohjelmoijan keksimä asiakasnumero, joka on luku väliltä 0-MAX_CLIENT.

Tietysti linkit pitää purkaa ohjelman lopuksi vaikkapa kutsulla:

	CloseAllDDEClients();

Siis vain kaksi kutsua ja automaattinen asiakaslinkki on valmis! Miksei tätä ole tehty Windowsiin valmiiksi?

Jos linkkiä halutaan muuttaa ohjelman suorituksen aikana, voidaan merkkijono selvittää jonkin Edit-kentän avulla ja sitten pyytää linkitystä samalle asiakasnumerolle. Tai voidaan jopa kutsua leikekirjasta linkin ottavaa muotoa:

	AutoLinkFromClipboard(n,hWnd,ID_TULOS);

Monimutkaisemmissa tapauksissa voidaan itse kirjoittaa funktio, joka lukee datan, käsittelee sen ja toimii sitten vaaditulla tavalla.

7.4.3 Malli kirjastojen käytöstä (DDE\AUTOLDDE.C)

Malliohjelma, jossa edellisiä kirjastoja on sovellettu, löytyy tiedostosta DDE\AUTOLDDE.C. Mallissa on avattu lisädialogi, joka toimii asiakkaana. Palvelimena toimii autolaskuri, josta saadaan palvelut Lkm!ha ja Lkm!ka. Palveluiden linkkitiedot voidaan siirtää leikekirjaan aktivoimalla vastaava nappula ja valitsemalla Edit/Copy.

Asiakasikkunaan voidaan laittaa asiakkaita joko leikekirjasta tai kirjoittamalla palvelimen tiedot Edit-ikkunaan (nämä menevät vain isoon asiakaskenttään). Asiakaskentät ovat kaikki monirivisiä Edit-ikkunoita, jotka Windows 3.1:en mahdollistamana on merkitty Read Only -muotoisiksi. Tällä on Static-ikkunaan verrattuna se etu, että kentät (ikkunat) voidaan tehdä aktiivisiksi ja niistä voidaan kopioida leikekirjaan staattista tietoa, kuitenkaan pystymättä muuttamaan kenttien sisältöä.

Asiakaskentät ovat monirivisiä, koska yksirivinen Edit-ikkuna ei osaa käsitellä CR/LF-merkkejä, vaan näyttää ne mustina laatikoina. Usein yksirivisenkin tiedon lopussa nimittäin on CR/LF (esim. Excelin solusta otettu linkki).

Oikeassa ohjelmassa asiakasikkunan ja palvelinikkunan ei välttämättä tarvitse olla erillisiä kuten tässä mallissa.

7.5 Yleistä OLEsta

Seuraava mystinen kirjainyhdistelmä DDE:n jälkeen on OLE (Object Linking and Embedding). OLE:n ydinajatus on erilaisten tietotyyppien (piirto-ohjelman kuva, taulukkolaskenta-arkin osa, tms.) yhdistäminen samaan dokumenttiin (teksti, taulukkolaskennan arkki. Tällä dokumentilla työskennellessään käyttäjän ei tarvitse suoraan tietää mistä dokumentin alkiot ovat peräisin. Riittää kun valitsee dokumentissa olevan alkion ja pääsee muokkaamaan sitä samalla työkalulla, jolla se on tehtykin.

OLE:ssa dokumentin palanen voi olla linkki (linking) alkuperäiseen tietoon, jolloin kaikki muutokset välittyvät käsiteltävään dokumenttiin. Tällöin palanen ei talletu dokumentin mukana vaan pelkkä tieto, mistä se on peräisin.

Toinen vaihtoehto on, että dokumentin alkio on upotettu (embedding), jolloin varsinainen tieto alkiosta on dokumentin mukana sekä tieto miten alkio on muodostettu. Kun tällainen alkio valitaan, kopioidaan alkio sen luoneelle ohjelmalle käsittelyyn ja käyttäjä voi muokata alkion sisältöä. OLE 2:ssa muokkaus tapahtuu vieläpä itse dokumentin sisällä, eli käyttäjä ei edes huomaa luoneen ohjelman käynnistymistä.

Upotukset ovat mukavia, mutta jos esimerkiksi MS Wordissä on kirjoitettu matemaattisia kaavoja upotusta (Equation) käyttäen, ei dokumentti enää siirrykään muihin käyttöjärjestelmiin (esim Maciin)! Samoin käy tietysti upotettujen kuvien kanssa.

7.6 Drag and Drop

"Drag and Drop" (lyhennetään jatkossa DaD, ei virallinen lyhenne) on menetelmä siirtää vetämällä tietoja paikasta toiseen. Tämä on eräänlainen ikkunoiden välisten viestien lähettämisen sovellutus.

Windows 3.1:ssä valmiina on FileManagerin DaD, jonka avulla voidaan välittää tiedostojen nimiä ohjelmalta toiselle (lyhennetään DaDF). DaD-palvelimessa valitaan joukko tiedostoja, palvelin muuttaa kursorin ulkonäön sallituksi ( tai ) jokaisen sellaisen ikkunan kohdalla, joka pystyy toimimaan DaD-asiakkaana ja kielletyksi ( ) sellaisten ikkunoiden kohdalla, jotka eivät hyväksy "pudotusta". Jos hiiren nappula vapautetaan asiakasikkunan kohdalla, saa asiakasikkuna tästä viestin ja se voi sitten lukea tiedostojen nimet palvelimelta ja tehdä näiden nimien perusteella mitä tahansa. Esimerkiksi MacIntoshin roskakoria vastaava toiminto voitaisiin helposti toteuttaa tällä periaatteella.

7.6.1 DaD palvelin (ALI\DROPFILE.C)

Yleisin DaD-palvelin on FileManager. Microsoft ei ole julkistanut DaDF-palvelimen toteuttamiseksi tarvittavia tietoja, mutta ainakin osittain Windows 3.1:ssä toimiva toteutus löytyy kirjasta [J.M. Richter: Windows 3.1: A Developer's Guide]. Tätä toteutusta matkien on tehty joukko aliohjelmia, joilla palvelimen toteutus käy jollakin seuraavista kutsuista:

	int DropFiles(HWND hWnd,LPCSRT files)  
	  - laittaa merkkijonossa files olevat tiedostot "pudotukseen"
	    merkkijonon muoto:
	         "hak1 t1 t2 t3;hak2 t4 t5 t6;hak3\t7"
	    esimerkiksi:
	         "C:\DOS SUBST.COM FC.EXE;C:\BIN\TGREP.COM
	int DropSelectFiles(HWND hWnd)
	  - näyttää tiedoston avausdialogin, antaa valita sieltä joukon tiedostoja
	    ja laittaa ne "pudotukseen"
	int DropListBoxFiles(HWND hWnd,UINT id,LPCSTR path)
	  - laittaa list-boxissa hWnd,id olevat tiedostojen nimet "pudotukseen"
	    kunkin nimen eteen lisätään polku path

Aliohjelmat hoitavat kursorin liikuttamisen, kursorin muodon vaihtamisen ja lopuksi tiedottamisen asiakasikkunalle. Jälleen yksi aliohjelma hoitamaan hommat ja yksi "miksei Windowsissa ole valmiina" kysymys lisää!

Toteutus perustuu DropAcceptFiles -aliohjelman käyttämän tietorakenteen "nuuskimiseen". FileManager hoitaa kuitenkin toiminnon viestien välityksellä ja tästä seuraa se, että FileManagerille ei saada pudotettua tiedostoja. Muille ohjelmille kylläkin. Joka tapauksessa jos joskus tulee dokumenttia siitä miten Drop tehdään, ei omassa ohjelmassa tarvitse tehdä muutoksia, vaan riittää päivittää DROPFILE.C.

Tiedostoon on laitettu lisäksi muutamia asiakastoimintoja helpottavia kutsuja:

	int DropFilesToListBox(HDROP hDrop,HWND hWnd,int id,int clear)
	  - ottaa Drop listassa olevat nimet ja laittaa ne list-boxiin hWnd,id.
	    clear == 1  => poistaa nimistä polkuosan
	          == 2  => tyhjentää ensin list-boxin
	          == 3  => molemmat edelliset ( == 1 | 2 )
	LPSTR DropFilesToString(HDROP hDrop,LPSTR s,int mb)
	  - ottaa Drop-listassa olevat nimet ja palauttaa ne muodossa
	      path1\n1;path2\n2;path3\n3

7.6.2 DaD esimerkki (C-ESIM\DAD\DADTEST.C)

DaD asiakkaana toimiminen on sitä vastoin yleisempää ja tähän löytyy Windowsin muihin kutsuihin verrattuna varsin helppokäyttöiset kirjastokutsut ja viesti:

	DragAcceptFiles    Aloitetaan ja lopetaan tiedostojen hyväksyminen 
	DragFinish         Vapautetaan siirtomuisti kun tiedot on otettu 
	DragQueryFile      Otetaan tiedostojen nimiä
	DragQueryPoint     Hiiren paikka, jossa vasen näppäin päästettiin ylös
	WM_DROPFILES       Viesti joka saadaan kun tiedostot pudotetaan

Seuraavassa esimerkki DaD-asiakkaana ja palvelimena toimimisesta:

	/*****************************************************************************
	  PROGRAM: DaDTest.c
	  PURPOSE: Malliohjelma Drag- and Drop käytöstä
	           Asiakas toiminnot (client, vastaanottaa pudotuksia) merkitty dadc
	           Palvelin (server) on merkitty dads
	  Editor:  Vesa Lappalainen  typistänyt malliohjelmista.
	  PROJECT: dadtest.c, dadtest.def
	           ALI\tabhand.c,  ALI\optdlg.c, ALI\mdialog.c
	           ALI\dropfile.c, ALI\clipboard.c, ALI\mjonot.c, ALI\dropfile.rc
	*****************************************************************************/
	#include <windows.h>
	#include <windowsx.h>
	#include <string.h>
	#include <shellapi.h> /* Drag and Drop, ks. BC esx. DragDrop */     /* dadc */
	#include "dropfile.h"                                               /* dads */
	#include "tabhand.h"
	
	#define ID_LISTBOX 100
	
	#define MAX_FILES 20
	static char FileStr[MAX_FILES+1][40] =
	  { "Vedä tiedostoja FileManagerista tai",
	    "WinCD:stä ja pudota tähän.",
	    "Maalaa sitten haluamasi ja vedä",
	    "ne vaikka WinCDhen!",
	    "Oikea näppäin antaa valita dialogista",
	    "ja keskinäppäin tyhjentää näytön",
	    ""};
	
	/****************************************************************************/
	TblClassSWindowMAIN("DADWClass",NULL,"Drag and Drop",MsgTbl,0);
	/****************************************************************************/
	
	/****************************************************************************/
	int TakeFiles(HDROP hDrop)
	{
	   int i,n = min(DragQueryFile(hDrop,-1,NULL,0),MAX_FILES); /* Nimien määrä */
	   for (i=0; i<n; i++) {
	     DragQueryFile(hDrop,i,FileStr[i],sizeof(FileStr[i]));  /* i:s nimi     */
	   }
	   FileStr[n][0]=0;
	   DragFinish(hDrop);   /* Muistilohkon vapautus!                           */
	   return n;
	}
	
	/****************************************************************************/
	/* Viestien käsittely:                                                      */
	/****************************************************************************/
	static EVENT WM_create(tMSGParam *msg)
	{
	  HWND lhWnd = CreateWindow("Listbox","Kissa",
	                WS_BORDER | LBS_MULTIPLESEL | WS_CHILD | WS_VISIBLE
	                | WS_HSCROLL | WS_VSCROLL |LBS_EXTENDEDSEL ,
	                10,10,280,250,msg->hWnd,(HMENU)ID_LISTBOX,
	                GetWindowInstance(msg->hWnd),NULL);
	  MoveWindow(msg->hWnd,10,10,320,400,FALSE);
	  SET_NOTIFYDRAG(lhWnd);                                            /* dads */
	  DragAcceptFiles(msg->hWnd,TRUE);                                  /* dadc */
	  return 0;
	}
	
	static EVENT WM_paint(tMSGParam *msg) /* # MAKE_DC # */
	{
	  int i,y = 250;
	  int dy = HIWORD(GetTextExtent(msg->hDC,"X",1));
	  for (i=0; FileStr[i][0]; i++)
	    TextOut(msg->hDC, 10, y+=dy, FileStr[i],strlen(FileStr[i]));
	  return 0;
	}
	
	static EVENT WM_dropfiles(tMSGParam *msg)                           /* dadc */
	{
	#if 0 /* Laita tähän 1,jos haluat tiedostojen nimet itse ikkunaan */
	  TakeFiles((HDROP)msg->wParam);       /* Oma funktio nimien ottamiseksi    */
	  InvalidateRect(msg->hWnd,NULL,TRUE); /* Huono tapa,mutta sopii lyhyeen mal*/
	#else
	  DropFilesToListBox((HDROP)msg->wParam,msg->hWnd,100,0);
	#endif
	  return 0;
	}
	
	static EVENT WM_mbuttondown(tMSGParam *msg)
	{
	  (void)ListBox_ResetContent(GetDlgItem(msg->hWnd,ID_LISTBOX));
	  return 0;
	}
	
	static EVENT WM_lbuttondown(tMSGParam *msg)                         /* dads */
	{
	  DropListBoxFiles(msg->hWnd,ID_LISTBOX,"");
	  return 0;
	}
	
	static EVENT WM_rbuttondown(tMSGParam *msg)                         /* dads */
	{
	  DropSelectFiles(msg->hWnd);
	  return 0;
	}
	
	static EVENT WM_destroy(tMSGParam *msg)
	{
	  DragAcceptFiles(msg->hWnd,FALSE);                                 /* dadc */
	  PostQuitMessage(0);
	  return 0;
	}
	
	/****************************************************************************/
	/* Viestien käsittelytaulukko                                               */
	/****************************************************************************/
	tMSGEntry MsgTbl[] = {
	  EV_HANDLE_WM_DESTROY,
	  { WM_BEGINDRAG , DoC , DoC , WM_lbuttondown },                    /* dads */
	  { WM_CREATE , DoC , DoC , WM_create }, /*a*/
	  { WM_DROPFILES , DoC , DoC , WM_dropfiles }, /*a*/
	  { WM_PAINT , DoC , DoC , WM_paint,  MAKE_DC  }, /*a*/
	  { WM_MBUTTONDOWN , DoC , DoC , WM_mbuttondown }, /*a*/
	  { WM_LBUTTONDOWN , DoC , DoC , WM_lbuttondown }, /*a*/
	  { WM_RBUTTONDOWN , DoC , DoC , WM_rbuttondown }, /*a*/
	  { WM_DESTROY , DoC , DoC , WM_destroy }, /*a*/
	  { 0 }
	};
	/****************************************************************************/

Huomattakoon, että tiedostojen nimet saadaan täydellisenä polkuna, joten niille on helppo tehdä haluttu operaatio, esimerkiksi tulostaa, tuhota, kopioida tai siirtää. Eli edellisestä mallista olisi saatu roskakori, jos se olisi pistetty näkymään aina ikonina ja kun nimet on noudettu, olisi kaikki tiedostot tuhottu.

7.6.3 DaDSave (WINDOWS\VLWINAPP\DADSAVE)

Edellisestä hyötykäyttöön kehitetty versio on DADSAVE.EXE. Ohjelmaa voidaan käyttää esimerkiksi siten, että kopioidaan leikekirjaan ohjelman kommenteissa olevat projektiin kuuluvat ohjelmien nimet ja pudotetaan ne listaan. Pudotuksessa valitut sanat korvautuvat, esimerkiksi ALI\ -> N:\KURSSIT\WINOHJ\ALI\. Sitten listasta nämä korvatut nimet voidaan vetää esimerkiksi Borlandin projektiin. Tai yleisimmin tarvittavat tiedostojen nimet on valmiiksi talletettu listaan, joista tarpeelliset voidaan poimia. Tarpeen vaatiessa pudotus voidaan tehdä "näppäin" tai "leikekirja" -pudotuksena, jos vastaanottava ohjelma ei ymmärrä oikeata DaDta.


previous next Title Contents