Oyun Programlamaya Giriş


Bölüm I : Windows Programlamaya Giriş
by Joseph "Ironblayde" Farrell

Çeviri: Hamdi Akoğuz

 

Giriş

Bu yazının amacı Windows programlamanın temellerini tanıtmaktır. Sonunda çok basit bir Windows programını yapabilir olacaksınız. Tek gerekli olan temel C bilgisidir. Kodlar içerisinde nadiren C++ eklentileri kullanacağım. Bununla birlikte Windows nesneye dayalı olduğu için biraz sınıf(class) bilgisi kimseyi incitmez. Eğer sınıflarla aranız çok iyi değilse telaşlanmayın çok karmaşık bir şeye ihtiyacınız olmayacak. Devam ettikçe ihtiyaç olduğu kadarını öğreneceksiniz. Tüm kod örnekleri Microsoft Visual C++ 6.0 derleyicisi ile test edildi.Eğer Win32 C/C++ derleyiciniz yok ise bir tane almalısınız. Hadi başlayalım!

Başlangıç

İhtiyacınız olan Windows fonksiyonlarının çoğunu içeren iki başlık dosyası windows.h ve windowsx.h . Bu ikisini programınıza eklediğinizden emin olun. Bunlardan başka C başlıklarını da kullanacağız. stdio.h,conio.h gibi.Ayrıca çoğu Windows programınızın başında kullanacağınız bir satır daha olacak:

#define WIN32_LEAN_AND_MEAN

Bu satır windows başlıklarından MFC ile ilgili bazı şeyleri devre dışı bırakır. Bu da programın bağlama zamanı azaltır. Oyun programlamada genellikle MFC gerekmeyeceği için bunu kullanmak iyi bir fikir olabilir. Eğer daha önce bu tip bir ifade görmediyseniz(#define ön işlemci komutu sadece bir isimle birlikte kullanılıyorsa) bu koşullu derleme denen bir tekniktir. Bu örneğe bakın:

#ifdef HATA_AYIKLAMA
    printf("Hata ayıklama modu aktif!");
#endif

Eğer derleyici bu kodu içeren programın başında #define HATA_AYIKLAMA komutunu okursa printf() ifadesi derlenir. Aksi halde yok sayılacaktır. Bu mantık hatalarını kontrol etmek için bir takım kodları ekleyip çıkarmaya yarar. WIN32_LEAN_AND_MEAN ın tanımlanmasıyla, Windows başlık dosyalarının nadiren kullanılan bileşenleri çıkarılmış olur.

WinMain() fonksiyonu

DOS-tabanlı C programlarının, çalıştırılmaya main() fonksiyonuyla başlaması gibi, Windows programları da WinMain() fonksiyonuyla başlar. Basit, boş bir WinMain() fonksiyonu şöyle gözükür:

int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
{
    return(0);
}

Hiçbir şey yapmayan, yanlızca bir değer döndüren bir fonksiyon için fazlasıyla garip gözüküyor! Sırayla gidelim. WINAPI bildiricisi de neyin nesi? Bu calling convention(çağırma düzeni) ile ilgili bir şey. Bu fonksiyona parametrelerin geçirilmesi, stack(yığın) temizliğini hangi fonksiyonun yapacağını ve hiç görmeyeceğiniz birkaç şeyi daha etkiler. “Calling convention” olarak WINAPI bildiricisine sahip olan bir fonksiyon parametreleri sağdan-sola değil de soldan-sağa geçirilir.Programlarınızda assembly kullanmayacaksanız detayları bilmenize gerek yok. WinMain() in WINAPI bildiricisine sahip olduğunu bilin yeter.

Fonksiyonun aldığı dört parametreyi inceleyelim:

HINSTANCE hinstance: Bu sizin programınızın örneğine (instance) bir tutamaktır(handle). Temel olarak, bunlar çalışmakta olan tüm programların izini sürmek için kullanılan işaretçilerdir. Birçok Windows fonksiyonu sizin programınızın örneğini(programın hafızadaki yeri) parametre olarak alır. Böylece gerçekleştireceği eylemi hangi uygulamaya yapacağını bilir.

HINSTANCE hPrevInstance: Artık kullanılmayan bir parametre olduğu için ilgilenmeye gerek yok. Eski Windows versiyonlarında, sizin programınızı çağıran programa bir tutamaktı. Hâlâ bulunmasının sebebi geriye dönük uyumluluk(backwards compatibility). Windows programlamada böyle şeylerle tekrar karşılaşacak sınınız.

LPSTR lpCmdLine: Program çalıştırılırken kullanılan komut satırı parametrelerini içeren stringe işaretçi. Komut satırı parametrelerinin sayısının tutulmadığına dikkat edin. Bunu kendiniz kontrol etmelisiniz.

int nCmdShow: Bu tamsayı ana pencerenin nasıl açılacağını gösterir. Bununla hiç uğraşmanıza gerek yok aslında. SW_ ile başlayan sabitlerin değerlerini alır. SW_SHOWNORMAL(ön tanımlı),SW_MAXIMIZE veya SW_MINIMIZE gibi.

WinMain()parametreleri bu kadar. Genellikle en önemlisi hinstance dır. Bir pencereyi oluşturma kısmına geçmeden önce Microsoft’un değişkenleri nasıl adlandırdığına bakmakta yarar var.

Hungarian Gösterim Sistemi

Microsoft değişkenler,sabitler ve sınıf adları için Hungarian gösterimi denen bir standart kullanır. Bunun örneğini WinMain() de gördük. Hungarian gösterimi veri tipini belli eden bir takım ön ekler kullanır.Bunlar:

b

BOOL (int)

by

BYTE or UCHAR (unsigned char)

c

char

cx, cy

short (genellikle uzunluklar; c "count" kelimesinden)

dw

DWORD (unsigned long)

fn

Fonksiyona işaretçi

h

Handle(tutamak) (Windows nesneleri için,bir çeşit işaretçi)

i

int

l

LONG (long int)

lp

Long pointer (32-bit)

msg

Message (bunu inceleyeceğiz)

n

tamsayı (short or int)

s

String

sz, str

Null-terminated ("ASCIIZ") string

w

WORD or UINT (unsigned int)

x, y

short (genellikle koordinatlar)

Bunlara ek olarak, birden fazla kelimeden oluşan değişken isimlerinde her kelimenin baş harfi büyük yazılır.Örneğin, oyuncu verisine işaretçi lpPlayerData olabilir. Bu standart gösterim genellikle kodun anlaşılırlığını artırır. Örneğin, WinMain() de hinstance ın handle, lpCmdLine ın 32bit işaretçi, nCmdShow un tamsayı olduğunu isimlerinden anlayabiliriz.

Hungarian gösterimi ile fonksiyon isimlendirme de aynı kuralları(önekler hariç) izler. Ör: ShowCharacterStats().

Sabitlerin isimlerinde ise tamamen büyük harf kullanılır. Kelimeleri ve önekleri ayırmak için de altçizgi (_) kullanılır. Ör: WIN32_LEAN_AND_MEAN. Windows da sık karşılaşacağınız bir şey, sabitlerin kullanılacakları fonksiyonun kısaltmasını önek olarak almasıdır. Örneğin; SW_SHOWNORMAL veya SW_MAXIMIZE ShowWindow() için kullanılan parametrelerdir.

Son olarak, sınıflar da fonksiyonlar gibi kullanılır. Bir farkla,C harfiyle başlarlar.  Cvehicle gibi.

Programlarınızda bunları kullanmak zorunda olmasanız da tüm Microsoft ürünleri bunu kullandığı için alışmaya çalışsanız iyi olur. Ben hâlâ üzerinde çalışıyorum.Her neyse,devam edelim…

 

 

Mesajlar

DOS’ta program yazarken çalışmakta olan diğer programlarla ilgilenmeniz gerekmez, çünkü DOS çok görevli(multitasking) bir işletim sistemi değildir. Fakat Windows’ta bu konu önemlidir. Bununla birlikte diğer nedenlerden dolayı da, Windows çalışmaktaolan uygulamalarla iletişim kurmak ve kullanıcı girdisi gibi birtakım olaylardan haberdar olmalarını sağlamak için Mesajları kullanır. Bir uygulamaya, penceresinin boyutlandırıldığı, taşındığı veya kapatılmakta olduğunu onlar bildirir. Yani doğru çalışabilmesi için, bir Windows programının bu mesajları kontrol edebilmesi lazımdır.

Bu, Geriçağırım (callback) denen özel fonksiyonlarla yapılır. Geriçağırım fonksiyonları aslında sizin yazdığınız bir kod ile çağırdığınız fonksiyonlar değildir. Bunun yerine, belirli olaylar onların çağrılmasına sebep olur.Böyle bir fonksiyon CALLBACK calling convention kullanılarak bildirilir veya tanımlanır.WinMain() deki WINAPI bildiricisini hatırlayın. Bu konuyu bir dakikalığına bırakacağım; çünkü,mesajları kontrol etmeden önce pencereyi oluşturmalısınız.

Pencere Sınıfları

İşte Burası biraz C++ bilmenin yararlı olacağı yer. Çünkü bir pencere oluşturmadan önce pencere sınıfını oluşturmalıyız. Bu sınıf pencereyle ilgili tüm bilgiyi(kullanacağı simge veya varsa menüsü gibi) içerecek. Yaptığınız her Windows programında ihtiyacınıza uygun bil pencere sınıfı oluşturmalısınız. Bunun için WNDCLASSEX isimli yapının elamanlarını doldurmalısınız. "EX" eki "extended"ın kısaltması, WNDCLASS isimli eski bir yapı daha var biz yenisini kullanacağız. İşte WNDCLASSEX yapısı:

 
typedef struct _WNDCLASSEX {
    UINT    cbSize;
    UINT    style;
    WNDPROC lpfnWndProc;
    int     cbClsExtra;
    int     cbWndExtra;
    HANDLE  hInstance;
    HICON   hIcon;
    HCURSOR hCursor;
    HBRUSH  hbrBackground;
    LPCTSTR lpszMenuName;
    LPCTSTR lpszClassName;
    HICON   hIconSm;
} WNDCLASSEX;
 

Pencere sınıfımız için bu yapının tüm elemanlarına gerekli değerleri vermelisiniz. Şimdi her kısmını inceleyelim:

 

UINT cbSize: Bu bayt olarak yapının boyutu. DirectX ile ilgilenirseniz bununla sık karşılaşacaksınız. Bu tip bir yapı(veya bu türden bir yapıya işaretçi), bir fonksiyona geçirilirken boyutun tekrar tekrar hesaplanması yerine buradan bakılabilir. Her zaman sizeof(WNDCLASSEX) olarak belirleyin.

UINT style: Pencere tipi. CS_ ile başlayan sabitleri alır.  | (bitwise OR,bit düzeyinde veya) operatorü ile birleştirerek birden fazla sabiti kullanabilirsiniz. Genellikle dört tane kullanacaksınız. Yazıyı kısa tutmak için sadece bunları açıklayacağım. MSDN Yardım’da tamamı detaylı olarak anlatılıyor. Visual C++ aldınız değil mi ?

CS_HREDRAW Pencere yatay olarak boyut değiştirdiğinde pencere içeriği yeniden çizilir.

CS_VREDRAW Pencere düşey olarak boyut değiştirdiğinde pencere içeriği yeniden çizilir.

CS_OWNDC    Her pencerenin kendi device context(aygıt bağlamı)’ine sahip olmasına izin verir.(Bu konu, bu yazının kapsamı dışında).

CS_DBLCLKS Pencere içerisindeki fare tıklarının tek mi çift mi olduğunun algılanmasını sağlar.

WNDPROC lpfnWndProc: Bu pencereye gönderilen mesajları yakalayacak geriçağırım (callback) fonksiyonuna işaretçi. Eğer fonksiyon işaretçilerini hiç kullanmadıysanız, fonksiyonun ismi aynı zamanda adresidir(parantezler olmadan). MsgHandler gibi bir fonksiyon ismi yazacaksınız.

int cbClsExtra: Bu fazladan sınıf bilgisi içindir. Çoğu programda buna ihtiyaç yoktur,özellikle oyunlarda. Sadece 0 a eşitleyin.

int cbWndExtra: Bir önceki gibi bunu da 0 a eşitleyebilirsiniz.

HANDLE hInstance: Bu pencere sınıfını kullanacak programın bir örneği(instance). WinMain()deki parametreyi hatırlayın; hinstance a eşitleyin.

HICON hIcon: Program simgesi için işaretçi. LoadIcon() fonksiyonuyla ayarlayın. Programlarınızda kaynakları(resource) kullanmayı öğrenene kadar genel bir sistem simgesine ayarlayabilirsiniz: LoadIcon(NULL, IDI_WINLOGO). IDI_ ile başlayan daha birçok sistem simgesi var. Yardım dosyalarına bakabilirsiniz.

 

HCURSOR hCursor: Bu program imleci için işaretçi. LoadCursor() fonksiyonu kullanılarak ayarlanır. Kendi imlecinizi kullanmak için program kaynaklarını* kullanabilmelisiniz. Bunu öğrenene kadar standart imleç kullanabilirsiniz: LoadCursor(NULL, IDC_ARROW).

HBRUSH hbrBackground: Eğer pencerenizin yenilenmesi gerekirse (boyutlandırma gibi durumlarda) en azından düş bir renk ile yada bir fırça tipiyle(desenli) boyanacaktır. Bu parametre ile bunu istediğiniz gibi ayarlayabilirsiniz. GetStockObject() fonksiyonuyla yükleyebileceğiniz birçok fırça tipi var. BLACK_BRUSH, WHITE_BRUSH, GRAY_BRUSH, gibi. Şimdilik GetStockObject(BLACK_BRUSH) kullanabilirsiniz. Bu fonksiyonlara çok kısa değindiğim için üzgünüm fakat aksi halde yazı çok uzayacak. Başka makalelerde hepsine değineceğim.Söz veriyorum!

LPCTSTR lpszMenuName: Pencerenizin bir menüye sahip olmasını istiyorsanız,pencereye ekleyeceğiniz menünün ismini burada belirtmelisiniz. Şimdilik menü yapmayı bilmediğiniz için NULL yapabilirsiniz.

LPCSTR lpszClassName: Sadece açıklama amaçlı sınıf ismi. İstediğiniz bir ismi verebilirsiniz. "Game_Class" gibi bir şey olabilir.

HICON hIconSm: Programın başlık çubuğunda ve görev çubuğunda gözükecek simgesi için. hIcon da olduğu gibi ayarlayabilirsiniz-- LoadIcon()fonksiyonunu kullanarak. Şimdilik standart simge için,LoadIcon(NULL, IDI_WINLOGO) ‘u kullanacağız.

İşte oldu! WNDCLASSEX ‘in tüm alanlarını tanımış oldunuz. Tamamını doldurunca bir pencere için hazır olacaksınız. Örnek bir sınıf şöyle olmalı:

 
WNDCLASSEX sampleClass;                                      // yapı için bir değişken tanımı
 
sampleClass.cbSize =        sizeof(WNDCLASSEX);              // Herzaman bunu kullanın!
sampleClass.style =         CS_DBLCLKS | CS_OWNDC |
                            CS_HREDRAW | CS_VREDRAW;         // standart ayarlar
sampleClass.lpfnWndProc =   MsgHandler;                      // bunu yazmak zorundayız!
sampleClass.cbClsExtra =    0;                               // ekstra sınıf bilgisi,gerek yok
sampleClass.cbWndExtra =    0;                               // ekstra pencere bilgisi,gerek yok
sampleClass.hInstance =     hinstance;                       // WinMain()deki parametre
sampleClass.hIcon =         LoadIcon(NULL, IDI_WINLOGO);     // Windows logo
sampleClass.hCursor =       LoadCursor(NULL, IDC_ARROW);     // standart imleç
sampleClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); //siyah
sampleClass.lpszMenuName =  NULL;                            // menü yok
sampleClass.lpszClassName = "Örnek sınıf"                    // sınıf ismi
sampleClass.hIconSm =       LoadIcon(NULL, IDI_WINLOGO);     // Windows logo
 

Şimdi hazırsınız. Fakat bir şeye değinmeliyim. GetStockObject() in sonucundaki  HBRUSH tip dönüşümüne dikkat edin. GetStockObject() bir çok nesneyi yüklemek için kullanılabildiğinden daha genel olan HGDIOBJ tipinde bir değişken döndürür. Visual C++’ın eski sürümlerinde tip dönüşümüne gerek yoktur fakat, VC++ 6.0 bu konuda biraz tutucu, tip dönüşümü yapmazsanız derlerken hata verecektir.

Yapmanız gereken son şey sınıfı Windows’a tanıtmak,bu sayede yeni sınıf kullanılabilir hale gelecek. Bu RegisterClassEx() isimli basit fonksiyonla yapılıyor. Aldığı tek parametre sınıfınızın adresi. Örnek sınıfımızı Windows’a kaydetmek için

RegisterClassEx(&sampleClass);
 

yeterli olacaktır. Windows yeni sınıftan haberdar olduğuna göre artık onu kullanarak bir pencere oluşturabiliriz. Artık zamanı geldi değil mi?

Pencereleri Oluşturmak

İyi haber yapmanız gereken tek şey CreateWindowEx() fonksiyonunu çağırmak. Kötü haberse; bu fonksiyon bir sürü parametre alıyor. Herhalde bu uzun listeler sizi yeteri kadar sıkmıştır.Yine de bu seferki çok da kötü değil. İşte, fonksiyon prototipi:

 
HWND CreateWindowEx(
    DWORD dwExStyle,      // gelişmiş pencere stili
    LPCTSTR lpClassName,  // sınıfınızın ismine işaretçi
    LPCTSTR lpWindowName, // pencerenizin ismine işaretçi
    DWORD dwStyle,        // pencere stili
    int x,                // pencerenin yatay konumu
    int y,                // pencerenin düşey konumu
    int nWidth,           // pencere genişliği
    int nHeight,          // pencere yüksekliği
    HWND hWndParent,      // ana pencereye işaretçi
    HMENU hMenu,          // menü veya alt pencereye işaretçi
    HINSTANCE hInstance,  // uygulama örneği için tutamak
    LPVOID lpParam        // pencere oluşturma verisine işaretçi
);
 

İlk şeyler ilk önce:geri dönüş değeri. Şu an itibariyle Windows’un kullandığı tüm bu çılgın veri tipleri tanıdık gelmeye başlamış olmalı.Eğer öyle değilse telaşlanmayın düşündüğünüzden daha çabuk alışacaksınız. HWND bir pencere tutamağı(bir çeşit işaretçi). CreateWindowEx() tarafından döndürülen değeri saklamak isteyeceksiniz. Çünkü birçok Windows fonksiyonu buna ihtiyaç duyacak. Şimdi parametrelerle uğraşmaya başlayalım. Zaten birçoğu kendini belli ediyor.

 

DWORD dwExStyle: Gelişmiş pencere nadiren kullanacağınız bir şey, o yüzden genellikle NULL yapın. Eğer merak ediyorsanız derleyicinizin yardım dosyalarında burada kullanabileceğiniz WS_EX_ ile başlayan bir sürü sabit bulabilirsiniz.

LPCTSTR lpClassName: Oluşturduğunuz sınıfa verdiğiniz ismi hatırlıyor musunuz? Sadece ismi vermeniz yeterli.

LPCTSTR lpWindowName: Bu pencerenin başlık çubuğunda görüntülenecek isim.

DWORD dwStyle: Pencerenin biçimini belirteceğiniz parametre. Burada da kullanabileceğiniz bir sürü WS_ ile başlayan sabit var ve | operatörüyle birleştirebilirsiniz. En genel olanları söyleyeceğim:

WS_POPUP                   Kenarlık ve başlığı dahi olmayan pencere.

WS_OVERLAPPED           Başlık çubuğu ve kenarlıklı pencere.

WS_OVERLAPPEDWINDOW Başlık çubuğu üzerinde tüm standart kontrollere sahip pencere.

WS_VISIBLE                Pencerenin ilk anda görülebilir olması için.

Aslında WS_OVERLAPPEDWINDOW sabiti de başka sabitlerin birleşiminden oluşuyor. Eğer ekranı kaplayabilen, simge durumuna küçülebilen, boyutlandırılabilen bir pencere istiyorsanız WS_OVERLAPPEDWINDOW kullanın. Eğer başlık çubuğu olan fakat boyutu sabit olan bir pencere isterseniz WS_OVERLAPPED kullanın. Hiçbir kontrole sahip olmayan bir pencere için de WS_POPUP kullanın. Böyle bir pencere sadece siyah(rengi biz belirledik) bir dikdörtgen oluşturur. Genellikle tam ekran oyunlar için bunu kullanacaksınız. Ayrıca pencerenin gözükmesini istemediğiniz durumlar dışında WS_VISIBLE ı da kullanmayı unutmayın. Bazı işlemler yaptıktan sonra pencerenin gözükmesini sağlayabilmek için de bunu kullanmayabilirsiniz.

int x, y: Oluşturulacak pencerenin sol üst köşesinin koordinatları.

int nWidth, nHeight: En,boy.

HWND hWndParent: Oluşturulacak pencerenin sahip olduğu bir ana pencere varsa bunu belirtmek için o pencerenin işaretçisi (handle)’ını kullanın. Bu genellikle düğme gibi kontroller için de kullanılır. Bir ana pencere oluşturuyorsanız NULL yapın,bu Windows masaüstü anlamına gelir.

HMENU hMenu: Pencereye eklemek isteyebileceğiniz menü tutamağı. Eğer programınızın kaynaklarından kullanacaksanız – nasıl yapıldığını öğrendikten sonra-- LoadMenu() fonksiyonunu kullanmalısınız. Menü kullanmayacaksanız NULL yapın.

HINSTANCE hInstance: Programın örneği(instance). WinMain() deki parametreyi kullanın.

LPVOID lpParam: Bu genellikle kullanmak isteyeceğiniz bir şey değil, özellikle oyunlarda ki sadece basit pencerelere ihtiyacımız var. Bu çoklu doküman ara yüzü (multiple document interfaces) gibi şeyler için kullanılır. Sadece NULL yapın.

En sonunda bir pencere oluşturmak için gerekli her şeye sahibiz.Bu işi yapacak örnek bir kod:

 
HWND hwnd;
if (!(hwnd = CreateWindowEx(NULL,                   // gelişmiş biçim,gerek yok
                            " Örnek sınıf ",        // sınıf adı
                            "Örnek Pencere",        // window title
                            WS_POPUP | WS_VISIBLE,  // pencere biçimi
                            0, 0, 320, 240,         // yer, boyut
                            NULL,                   // sahibine tutamak(masaüstü)
                            NULL,                   // menü tutamağı (yok)
                            hinstance,              // porgram örneğine tutamak
                            NULL)))                 // ne gerek var?
    return(0);
 

Bu bir oyun için kullanmak isteyeceğiniz bir pencere. Çünkü hiçbir kontrole sahip değil(popup) pencere. CreateWindowEx() fonksiyonunu bir if deyimi içerisinde kullandığıma dikkat edin. Eğer CreateWindowEx() başarısız olursa NULL döndürür.  Bu şekilde eğer herhangi bir sebepten dolayı pencere oluşturulamazsa WinMain() sadece 0 döndürür ve program sonlanır.

Şimdi kullanılabilir bir pencere oluşturabilecek Windows programı yazabilecek duruma geldiniz. Neredeyse. "Örnek Sınıf"ı oluştururken mesajları kontrol eden bir fonksiyona işaretçiye ihtiyacımız olmuştu. Windows’un bir şeyler oluşturmamıza izin verebilmesi için o fonksiyonu yazmalıyız.

Mesajları İşlemek

Windows mesajlarına biraz değinmiştik. Şimdi nasıl kullanacağımızı göreceğiz.Bir mesaj kontrol fonksiyonun prototipi şöyledir:

 
LRESULT CALLBACK MsgHandler(
    HWND hwnd,     // pencere tutamağı
    UINT msg,      // mesaj tanımlayıcı
    WPARAM wparam, // mesaj parametreleri
    LPARAM lparam  // daha çok mesaj parametresi
};
 

LRESULT geri dönüş tipi özel olarak mesaj işleme fonksiyonları tarafından kullanılan bir tiptir. CALLBACK çağırma kuralından daha önce bahsetmiştim.Parametreler de çok basit:

HWND hwnd: İşleyeceğimiz mesajı gönderen pencere.

UINT msg: Mesaj tanıtıcı. Bu parametrenin sahip olabileceği değerler WM_ ("Windows message") ile başlayan sabitler. Gönderilebilecek değişik tipte mesaj sayısı saçmalık derecesinde fazla,fakat en önemlileri bunlar:

WM_ACTIVATE         Yeni bir pencerenin aktif olması anında.

WM_CLOSE              Bir pencere kapatılırken.

WM_COMMAND           Bir menü seçeneğinin seçilmesi.

WM_CREATE            Pencerenin oluşturulması.

WM_LBUTTONDBLCLK Sol fare düğmesi çift tıklanırsa.

WM_LBUTTONDOWN    Sol fare düğmesine basılması.

WM_MOUSEMOVE        Fare hareketi.

WM_MOVE                Pencerenin taşınması.

WM_PAINT              Pencerenin bir kısmının yeniden çizilmesi gerekirse.

WM_RBUTTONDBLCLK Sağ fare düğmesi çift tıklanırsa.

WM_RBUTTONDOWN    Sol fare düğmesine basılırsa.

WM_SIZE                Pencerenin boyutu değişirse.

WM_USER                İstediğiniz başka bir olay için kullanabilirsiniz.

WPARAM wparam, LPARAM lparam: Bu parametrelerin kullanımı hangi mesajın gönderildiğine bağlı,fakat genel olarak mesajın anlamının tam olarak belirlenmesi için.

Eğer pencerenizin alabileceği her mesajı kontrol için kod yazmak zorunda olsaydınız herhalde deli olurdunuz. Ben kesinlikle olurdum! Şükür ki, Windows varsayılan bir mesaj işleyiciye sahip. Eğer bir olay için özel olarak olmasını istediğiniz bir şey yoksa, her zaman DefWindowProc()u çağırabilirsiniz. Buna göre tamamen doğru çalışan basit bir mesaj işleyici fonksiyon yazabiliriz:

 
LRESULT CALLBACK MsgHandler(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
    return(DefWindowProc(hwnd, msg, wparam, lparam));
}
 

Basit, değil mi? Genellikle bazı mesajları kendiniz işlemek isteyeceksiniz. Bu durumda kendi kodunuzu yazıp,işinizin bittiğini belirtmek için 0 döndürebilirsiniz. Örnek olarak ;Pencere oluşturulduğunda oyunu başlangıç durumuna getiren ve diğer herhangi bir mesaj için standart mesaj işleyiciyi çağıran bir fonksiyon:

 
LRESULT CALLBACK MsgHandler(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
    if (msg == WM_CREATE)      //Her hangi bir mesaj oluşturulduğunda bunun WM_CREATE olup olmadığını kontrol et
    {
        Initialize_Game();     
        return(0);             //Bu mesaj için ihtiyacımız olan kodun bittiğini belirtmek için
    }
 
    return(DefWindowProc(hwnd, msg, wparam, lparam));
}
 

Sizin kullanacağınız mesaj işleyici fonksiyon sonunda koca bir switch deyimine sahip olacaktır. Şimdi her şeyin doğru çalışabilmesi için göstereceğim bazı şeyler daha var.

Mesaj Kuyruğunu Okumak

Programınızın ana döngüsünün başlarında, mesaj kuyruğunda(oluşan tüm mesajların sıraya konulur) sizi bekleyen birşey olup olmadığını kontrol etmelisiniz.Eğer öyleyse mesaj işleyiciniz işini doğru yapabilmesi için birkaç şey yapmalısınız. Bunun için ihtiyacınız olan PeekMessage() fonksiyonu. İşte prototipi:

 
BOOL PeekMessage(
    LPMSG lpMsg,         // mesaj yapısına işaretçi
    HWND hWnd,           // pencere tutamağı
    UINT wMsgFilterMin,  // ilk mesaj
    UINT wMsgFilterMax,  // son mesaj
    UINT wRemoveMsg      // kaldırma bayrağı
);
 

Geri dönüş tipi, BOOL, bir int, fakat sadece iki değer alabilir: TRUE veya FALSE. Eğer bir mesaj kulrukta bekliyorsa, fonksiyon TRUE döndürür. Aksi halde, FALSE döndürür. Parametreler de çok açık:

LPMSG lpMsg: MSG tipinden bir değişkene işaretçi. Eğer bir mesaj bekliyorsa, bu değişken gerekli mesaj bilgisiyle doldurulur.

HWND hWnd: Mesaj kuyruğu kontrol etmek istediğiniz pencere.

UINT wMsgFilterMin, wMsgFilterMax: Kuyrukta kontrol edilecek ilk ve son mesajın numaraları.Genellikle kuyruktaki ilk mesajla ilgileneceğiniz için her iki parametreyi de 0 yapın.

UINT wRemoveMsg: Bu sadece iki değer alabilir, PM_REMOVE yada PM_NOREMOVE. Bir mesajı okuduktan sonra kuyruktan çıkarmak için birincisini kullanın. İkinci ise mesajın okunduktan sonra da kuyrukta kalmasını sağlar.Genellikle PM_REMOVE kullanırsınız.

Eğer bir mesaj bekliyorsa mesaj işleyici fonksiyonunuzu çağırmalısınız. Bun için iki fonksiyon çağrısı kullanmalısınız: birincisi TranslateMessage() ve diğeri DispatchMessage(). Prototipleri de çok benzer:

 
BOOL TranslateMessage(CONST MSG *lpmsg);
LONG DispatchMessage(CONST MSG *lpmsg);
 

Tahmin edebileceğiniz gibi birincisi mesajda bir takım çevirmeler yapar ve ikincisi ise sizin mesaj işleyici fonksiyonunuzu harekete geçirir ve ona MSG yapısından gerekli bilgiyi iletir. Bilmeniz gereken sadece bu kadar! Ana döngünüzün her tekrarında, eğer bir mesaj bekliyorsa bu iki fonksiyon çağrılır ve MsgHandler() fonksiyonunuz gerisini halleder. Örnek kod:

 
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}
 

Problem yok! Artık bir pencere sınıfı oluşturan, onu Windowsa tanıtan(register), bir pencere ve geçerli bir mesaj işleyiciye sahip Windows programı yazabilirsiniz. Fena değil,değil mi? Bu yazıyı bitirmeden önce bir iki şeye daha değinmek istiyorum. Mesajlar konusundayken, kendinizin mesaj göndermek isteyeceğiniz zamanlar gelecek. İşte nasıl yapacağınız.

Mesaj Göndermek

Bunun iki yolu var. PostMessage() veya SendMessage() fonksiyonlarını kullanabilirsiniz. Prototipleri çok benzer:

 
BOOL PostMessage(
    HWND hWnd,      // hedef pencere tutamağı
    UINT Msg,       // gönderilecek mesaj
    WPARAM wParam,  // ilk mesaj parametresi
    LPARAM lParam   // ikinci mesaj parametresi
);
 
LRESULT SendMessage(
    HWND hWnd,      // hedef pencere tutamağı
    UINT Msg,       // gönderilecek mesaj
    WPARAM wParam,  // ilk mesaj parametresi
    LPARAM lParam   // ikinci mesaj parametresi
);
 

Parametreler MsgHandler() fonksiyonunda kullandıklarımızın aynıları o yüzden tekrarlamayacağım.Tek bilmeniz gereken bu iki fonksiyonun farkı, bu nedenle her ikisini de sade bir şekilde inceleyeceğim.

PostMessage() fonksiyonu bir mesajı sadece mesaj kuyruğuna eklemek ve mesajın işlenmesinin program akışına bırakılmak istendiği durumlarda kullanılır. Fonksiyon başarılı olursa (TRUE) ,başarısız olursa (FALSE) döndürür. Sadece istenilen mesajı kuyruğa ekler ve değeri döndürür.Genellikle PostMessage()işinizi görecektir.

SendMessage ise biraz farklı. Geri dönüş değerinin LRESULT olduğuna dikkat edin. Bu veri tipi mesaj işleme fonksiyonları kullanır. Çünkü SendMessage(), bir mesajı kuyruğa eklemez – mesajı işlenebilecek hale çevirip hemen mesaj işleyici fonksiyonunuzu çağırır. Mesajın işlenmesi bitene kadar bir değer döndürmez. SendMessage(), daha yüksek öneme sahip, hemen gerçekleşmesi gereken olaylar için kullanılır. Anında gerçekleşmesini istediğiniz şeyler için kullanın.

Mesajlar konusu bizi son konumuza taşıyor. Windows programlama ile DOS programlaması arasındaki fark...

Program Akışı

DOS’ta, mesajlarla falan uğraşmanıza gerek yok. Aynı anda çalışan birçok programla ilgilenmenize gerek yok. Fakat Windows’ta program yazarken, bunlar çok önemli şeyler. Dolayısıyla yazdığınız program DOS’takinden daha farklı çalışmak zorunda. Şu yalancı kodu inceleyin:

 
// ana oyun döngüsü
do
{
    // mesajların kontrolü
 
    // ...
 
    // eğer gerek varsa ekranı yenile
    if (new_screen)
    {
        FadeOut();
        LoadNewMap();
        FadeIn();
    }
 
    // oyun mantığını işlet
    WaitForInput();
    UpdateCharacters();
    RenderMap();
 
} while (game_active);
 

FadeOut() fonksiyonunun şöyle çalıştığını farz edelim: fonksiyon çağrıldığında bir saniye kadar bir zamanda ekranı karartır. Ekran tamamen siyah olduğunda bir değer döndürür ve durur. FadeIn() de benzer şekilde çalışır. WaitForInput() Bir tuşa basılana kadar bekler. Belki de girdiyi global bir değişkende falan saklar. Bu DOS’ta çok iyi çalışabilir fakat bir Windows oyununda, kesinlikle böyle olmaz!

Neden? Eğer new_screen true değerini alırsa ne olur? Ekran kararır harita yüklenir ve görüntü tekrar belirir. Bu yaklaşık iki saniye alır. Bu iki saniye içerisinde kullanıcı pencereyi simge durumuna küçültürse ne olur? Program hiçbir şey olmamış gibi çalışmaya devam eder. Bu hatalı çıktı ,koruma hatası vs. oluşturabilir. Çok açık ki bu kabul edilemez. WaitForInput() fonksiyonu daha beter olur, çünkü bir tuşa basılana kadar programı bekletir. Önceki örnekteki gibi bir durumda daha kötü hatalar oluşabilir, neredeyse kesinlikle olur.

Bir başka şey de, eğer oyun 30 FPS’de çalışıyorsa, oyun döngüsünün saniyede 30 kere tekrar ettiğinden emin olmalısınız. Döngünün her yinelemesinde tek bir resim çizilmeli. Yani örnekteki FadeOut() gibi bir fonksiyon daha farklı şekilde çalışmalı. Bu ilk bakışta bir engel gibi gözükebilir fakat sadece değişik bir düşünce tarzıdır. Bir kere buna alışırsanız daha organize ve esnek programlar yazabilirsiniz.

Son

Temel Windows programlama bu kadar. Örnekte sadece bir pencere gösterebilen bir program yazmış olsak da daha gelişmiş Windows programlarına temel teşkil ediyor. Bir daha ki sefere program kaynaklarını anlatacağım. Simge,imleç,ses,menü ve daha birçok şeyi .EXE ‘nizin içerisine yerleştirebileceksiniz!

Çeviri: Hamdi AKOĞUZ akoguzh@itu.edu.tr