Java ile Nesne Yönelimli Programlama · Ders Notu

Bölüm 9: Grafikler

Şimdiye kadarki programlar konsolda çalıştı: soruyu program sordu, kullanıcı klavyeden yanıt verdi, sonuç alt alta yazıldı. Gündelik hayatta kullanılan programlar ise pencere, düğme, metin alanı ve çizimlerle çalışır. Bu not Java'da grafik kullanıcı arayüzü kurmayı anlatır. Önce olay güdümlü programın konsol programından farkı, AWT ve Swing kütüphaneleri ve ekran koordinatları ele alınır. Ardından JFrame ve JPanel ile pencere kurmak, paintComponent ve Graphics ile çizmek, Point, Color ve Font sınıfları gelir. Yerleşim yöneticileri bileşenleri pencereye dizer; düzenin piksel piksel nasıl hesaplandığı adım adım izlenir. Fare olayları, dinleyici arayüzleri, adaptör sınıfları ve olay iş parçacığı notun merkezindedir. Not, üç küçük uygulamayla biter: fareyle çizim programı, metin dönüştürücü ve para üstü hesaplayıcı.

Öğrenme çıktıları

Pencere kurmak
JFrame ve JPanel ile pencere ve panel oluşturmak, bileşen eklemek, pencereyi göstermek ve kapatmak.
Çizmek
paintComponent metodunu geçersiz kılıp Graphics ile şekil ve metin çizmek; ekran koordinatlarını ve sınırlayıcı dikdörtgeni kullanmak.
Yerleştirmek
BorderLayout, FlowLayout, GridLayout ve BoxLayout ile bileşenleri düzenlemek; bileşenlerin konum ve boyutunu hesaplamak.
Olaylara yanıt vermek
Dinleyici arayüzleri, adaptör sınıfları, anonim sınıflar ve lambda ifadeleriyle fare, düğme ve metin olaylarını işlemek.
Güvenli başlatmak
Arayüzü SwingUtilities.invokeLater ile olay iş parçacığında kurmanın nedenini açıklamak; invokeLater ile invokeAndWait arasındaki farkı izlemek.
Uygulama yazmak
Çizim programı, metin dönüştürücü ve para üstü hesaplayıcıyı okuyup değiştirebilmek.

9.1Grafik programlama

9.1.1Bileşen, olay ve görsel oluşturma

Bir kullanıcı bilgisayarı çoğunlukla pencereler üzerinden kullanır. Pencerenin başlığı, düğmeleri, menüleri ve metin alanları vardır. Kullanıcı fareyle tıklar, klavyeyle yazar, pencereyi büyütür. Program da bu eylemlere yanıt verir. Bu türden bir arayüze grafik kullanıcı arayüzü denir.

Tanım 9.1

Grafik kullanıcı arayüzü (graphical user interface, GUI), kullanıcının pencere, düğme, menü, metin alanı ve çizim gibi görsel öğelerle programla etkileştiği arayüzdür.

Java, GUI geliştirmek için kapsamlı bir sınıf kütüphanesi sunar. Bu kütüphaneyle bir GUI programı yazmak için üç kavramı anlamak gerekir.

  • Bileşen (component): Pencere, düğme, etiket ve metin alanı gibi arayüzün yapı taşlarıdır. Her bileşen bir sınıfın nesnesidir. Bir düğme, JButton sınıfından oluşturulmuş bir nesnedir; metni, rengi ve boyutu bu nesnenin alanlarında saklanır.
  • Olay işleme (event handling): Fare tıklaması, tuşa basma gibi kullanıcı eylemleri yakalanır ve bunlara yanıt veren metotlar çağrılır.
  • Görsel oluşturma (painting): Şekiller çizilir, ekran güncellenir ve bileşenler görüntülenir.
bileşenleri kurolay bekleolay oluştudinleyici çalışıriş bitince yeniden beklemeye dönertıklama, tuş, fareactionPerformed…
Şekil 9.1: GUI programının yaşamı. Bileşenler kurulur, program olay bekler, bir olay gelince ilgili dinleyici metot çalışır; ardından program yeniden bekler.

Bir GUI programı, müşteriyi bekleyen bir dükkâna benzer. Önce vitrin hazırlanır: bileşenler oluşturulur ve pencereye yerleştirilir. Sonra program bekler. Müşteri ne zaman neye dokunursa o işle görevli kişi çalışır. Programda vitrinin karşılığı bileşenler, dokunuşun karşılığı olay, görevlinin karşılığı da dinleyici metottur (Şekil 9.1).

9.1.2Konsol programı ve GUI programı

Konsol programında akış sıralıdır. main metodu baştan sona çalışır. Programın bir girdiye ihtiyacı olduğunda soruyu program sorar ve Scanner ile klavyeden bir satır okur. Sonuç System.out.println ile yazılır. main bitince program da biter.

GUI programının akışı başkadır. main yalnız arayüzü kurar ve biter. Program bundan sonra kullanıcının hamlesini bekler. Kullanıcı bir düğmeye bastığında ya da panele tıkladığında ilgili metot çalışır. Bu çalışma biçimine olay güdümlü programlama denir.

Tanım 9.2

Olay (event), kullanıcının tıklama, tuşa basma, fareyi sürükleme gibi bir eylemi ya da sistemden gelen bir bildirimdir. Olay güdümlü (event-driven) programda akışı koddaki satır sırası değil, olayların geliş sırası belirler.

Tablo 9.1: Konsol programı ile GUI programının karşılaştırması.
Konsol programıGUI programı
AkışSıralı: main baştan sona çalışır, soruyu program sorarOlay güdümlü: program kullanıcının hamlesini bekler
GirdiScanner ile klavyeden satır okunurDüğme, metin alanı, fare; dinleyici metotlar çağrılır
ÇıktıSystem.out.printlnBileşenlerin metni değişir ya da panel yeniden çizilir
Bitişmain bitince program bitermain bitse de pencere kapanana kadar çalışır
Kütüphanejava.util, java.iojava.awt, javax.swing, java.awt.event

Tablodaki son satırların ikisi önemlidir. GUI programında main bittiği hâlde program sürer. Bunun nedeni, Java'nın pencere açılınca ayrı bir iş parçacığı başlatmasıdır. Bu iş parçacığı olay kuyruğunu bekler ve gelen her olayı ilgili dinleyiciye iletir. JVM, bu iş parçacığı çalıştıkça kapanmaz.

GUI programında “sonra ne olacak?” sorusunun cevabı koddaki sıra değildir; kullanıcının sıradaki eylemidir. Bu yüzden kodun büyük kısmı “şu olursa şunu yap” biçimindeki dinleyicilerden oluşur. Programcı her eylem için bir yanıt yazar, ama eylemlerin hangi sırayla geleceğini bilmez.

Sık yapılan hata

GUI programında kullanıcı girdisini bir while döngüsünde beklemeye çalışmak. Konsol alışkanlığıyla yazılan “kullanıcı düğmeye basana kadar dön” döngüsü olay iş parçacığını meşgul eder ve pencere donar. GUI programında beklemeyi Java yapar; programcı yalnız dinleyiciyi kaydeder.

9.1.3AWT ve Swing

Java'nın grafik sınıfları iki kütüphaneye dağılmıştır. İkisi de JDK ile gelir; ayrıca bir şey kurmak gerekmez.

  • AWT (Abstract Window Toolkit), java.awt paketi: Java'nın ilk grafik kütüphanesidir. Bugün daha çok ortak sınıfları için kullanılır: Graphics, Color, Font, Point, yerleşim yöneticileri ve olay sınıfları (java.awt.event).
  • Swing, javax.swing paketi: AWT'nin üzerine kurulmuş, bileşenleri Java'nın kendisinin çizdiği kütüphanedir. Sınıf adları J ile başlar: JFrame, JPanel, JButton, JLabel.

İki kütüphane arasındaki fark bileşenlerin nasıl çizildiğindedir. AWT bir düğme istediğinde işletim sisteminin kendi düğmesini kullanır. Bu yüzden aynı program Windows'ta ve Linux'ta farklı görünür. Swing ise düğmeyi kendisi boyar. Görünüm her sistemde aynıdır ve Java'nın denetimindedir. Bu notta bileşenler Swing'den, çizim ve yardımcı sınıflar AWT'den gelir. Tipik bir GUI programı bu yüzden iki paketi birlikte içe aktarır: import java.awt.*; ve import javax.swing.*;.

ComponentContainerWindowJComponentFrameJFrameJPanelJButtonJLabelJScrollPaneJTextComponentJTextFieldJTextAreajava.awtjavax.swing
Şekil 9.2: GUI sınıflarının kalıtım ağacı. İçi boş üçgen “türetilmiştir” anlamındadır. JFrame bir AWT penceresidir; diğer Swing bileşenleri JComponent'ten türer.

Şekil 9.2, kalıtımın GUI kütüphanesinde nasıl kullanıldığını gösterir. Ağacın kökünde Component vardır: ekranda yeri ve boyutu olan her şey bir bileşendir. Container, başka bileşenleri içinde tutabilen bileşendir. Container'dan iki dal ayrılır. Birinci dalda Window, Frame ve en sonda JFrame bulunur. İkinci dalda Swing bileşenlerinin ortak üst sınıfı JComponent vardır. JPanel, JLabel ve metin bileşenleri doğrudan ya da kısa bir yoldan JComponent'ten türer. JButton ise araya AbstractButton sınıfını alır.

Bu ağacın pratik bir sonucu vardır. Bir metot parametre olarak Component bekliyorsa ona düğme de, etiket de, panel de verilebilir. Çok biçimlilik burada gündelik bir araçtır. Örneğin panel.add(...) metodu herhangi bir bileşeni kabul eder.

9.1.4Ekran koordinatları

Grafik bir uygulama üç adımda kurulur. Önce Java'nın grafik sınıflarıyla arayüze bileşenler eklenir. Sonra kullanıcının eylemlerine yanıt veren olay işleme metotları tanımlanır. Son olarak paint() ya da paintComponent() gibi metotlarla görsel oluşturulur. Çizime geçmeden önce ekranın nasıl adreslendiğini bilmek gerekir.

Ekran bir piksel ızgarasıdır. Her pikselin bir sütun numarası (x) ve bir satır numarası (y) vardır. Koordinatlar tam sayıdır ve bileşenin sol üst köşesinden ölçülür.

x = 0x = getWidth()y = 0y = getHeight()(0, 0) sol üst köşex sağa, y AŞAĞI doğru artar
Şekil 9.3: Ekran koordinat sistemi. Orijin sol üst köşededir; x sağa, y aşağı doğru büyür. Çizgi, dikdörtgen ve elips piksellerden oluşur.
Tanım 9.3

Ekran koordinatları, bir bileşenin içindeki pikseli (x, y) tam sayı çiftiyle gösterir. (0, 0) bileşenin sol üst pikselidir. x sağa, y aşağı doğru büyür. Sağ alt piksel (getWidth() − 1, getHeight() − 1)'dir.

Matematikteki eksenle en önemli fark y'nin yönüdür. Matematikte y yukarı doğru büyür; ekranda aşağı doğru. Bu tercih ekranın tarihinden gelir: katot ışınlı tüpler görüntüyü üstten alta, satır satır çiziyordu. Sonuç olarak y değeri büyüyen bir nokta ekranda aşağı iner. y'yi azaltmak noktayı yukarı taşır.

Örnek 9.1

Bir panel 300 piksel genişliğinde ve 160 piksel yüksekliğindedir. Sağ üst, sol alt ve orta pikselin koordinatları nedir?

Sütunlar 0'dan 299'a, satırlar 0'dan 159'a numaralanır. Sağ üst piksel (299, 0), sol alt piksel (0, 159)'dur. Ortadaki piksel için genişlik ve yükseklik ikiye bölünür: (300 / 2, 160 / 2) = (150, 80). Sayılar tek olsaydı tam sayı bölmesi ondalığı atardı; örneğin 261 / 2 = 130 olur.

9.1.5Tuval, kalem, renk ve yazı tipi

Çizim için üç şey gerekir: üzerine çizilecek bir yüzey, çizimi yapan bir kalem ve kalemin özellikleri.

  • Tuval (canvas): Üzerine resim çizilebilen yüzeydir. Swing'de genellikle JPanel kullanılır. AWT'de bu iş için Canvas sınıfı vardır. Programcı JPanel'den bir alt sınıf türetir ve çizimi onun içine yapar.
  • Graphics sınıfı: Çizim işlemlerini yapan “kalem”dir. Metotları drawLine(), drawRect(), fillOval(), drawString() ve drawImage() gibi adlar taşır.
  • Color ve Font: Kalemin rengini Color, yazı tipini Font belirler: g.setColor(Color.RED), g.setFont(new Font("Serif", Font.BOLD, 24)).
JFrame (pencere)JPanel: tuvalMerhabaGraphics gsetColor / setFont: kalemi değiştirirfillRect, drawOval, drawString
Şekil 9.4: JFrame penceresinin içinde bir JPanel tuvali. Çizimi Graphics nesnesi yapar.

Kalem benzetmesi bir kuralı da açıklar. Graphics nesnesi programcının elindeki kalemdir. setColor kalemi değiştirir. Kalem değiştirilene kadar bütün çizimler aynı renkle yapılır. Yazı tipi için de durum aynıdır: setFont çağrısından sonraki bütün drawString'ler yeni yazı tipini kullanır. Graphics nesnesi bu yüzden durum tutan bir nesnedir: kendi rengini ve yazı tipini hatırlar.

9.1.6paintComponent ile çizim

Bir panele kalıcı çizim yapmanın yolu, JPanel'den türetilen bir sınıfta paintComponent metodunu geçersiz kılmaktır.

import java.awt.*;
import javax.swing.*;

class CizimPaneli extends JPanel {
    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);     // zemini temizle
        g.setColor(Color.BLUE);
        g.drawRect(20, 30, 50, 70);  // x, y, en, boy
        g.drawString("Merhaba, Java Grafik!", 100, 100);
    }
}

Sınıf JPanel'i genişletir ve üst sınıftaki paintComponent(Graphics g) metodunu geçersiz kılar. @Override notu derleyiciye “bu metot üst sınıftaki bir metodun yerini alıyor” der. Metot adı yanlış yazılırsa derleyici bu notu görüp hata verir. Gövdenin ilk satırı super.paintComponent(g)'dir: üst sınıfın kendi çizimi, yani zeminin arka plan rengiyle boyanması yapılır. Ardından kalem maviye çevrilir. drawRect(20, 30, 50, 70) sol üst köşesi (20, 30)'da olan, 50 piksel geniş ve 70 piksel yüksek bir dikdörtgen çerçevesi çizer. Son satır (100, 100) noktasına bir metin yazar.

Merhaba, Java Grafik!
Şekil 9.5: Panel 300 × 160 iken CizimPaneli'nin çıktısı. Mavi çerçeve ve aynı renkte metin.

Bu kodda dikkat çeken bir şey vardır: paintComponent'i programın hiçbir yeri çağırmaz. Metodu Swing çağırır. Pencere ilk açıldığında, boyutu değiştiğinde, üstündeki başka bir pencere çekildiğinde ya da program repaint() ile yeniden çizim istediğinde Swing bu metodu çalıştırır. Çağırırken de hazır bir Graphics g nesnesi verir. Bu nesne o anda çizilecek bölgeye bağlıdır.

Metodun gerçekte hangi çizim çağrılarını yaptığını görmek için Swing'in DebugGraphics sınıfı kullanılabilir. Bu sınıf, her çizim çağrısını bir satır olarak yazan bir kalemdir. CizimPaneli 300 × 160 boyutunda bir resim üzerinde bu kalemle çalıştırıldığında şu çağrılar görülür (kısaltılmış):

setColor(238, 238, 238)
fillRect(0, 0, 300, 160)
setColor(0, 0, 255)
drawRect(20, 30, 50, 70)
drawString("Merhaba, Java Grafik!", 100, 100)

İlk iki satırı programcı yazmadı. Onlar super.paintComponent(g)'nin işidir: kalem panelin arka plan rengine, (238, 238, 238) açık griye çevrilir ve bütün panel bu renkle doldurulur. Sonraki üç satır programcının yazdığı çağrılardır.

Tanım 9.4

paintComponent(Graphics g), bir Swing bileşeninin kendi içeriğini çizen metottur. Swing bileşeni yeniden boyaması gerektiğinde bu metodu çağırır. Kendi çizimini yapmak isteyen sınıf metodu geçersiz kılar ve gövdeye önce super.paintComponent(g) yazar.

Bir grafik nesnesi panel.getGraphics() ile de alınabilir. Ancak bu nesneyle yapılan çizim kalıcı değildir. Pencere bir sonraki kez yeniden çizildiğinde, örneğin küçültülüp büyütüldüğünde, Swing paintComponent'i çağırır ve getGraphics() ile çizilenler silinir. Kalıcı çizimin yolu paintComponent'tir. Çizimin verisi alanlarda saklanır ve metot her çağrıldığında bu veriden yeniden üretilir.

Metnin konumu da bir ayrıntı taşır. drawString'in y parametresi metnin üst kenarı değil, taban çizgisidir. Taban çizgisi harflerin “oturduğu” çizgidir. drawString("Merhaba, Java Grafik!", 100, 100) çağrısında harflerin gövdesi 100. satırın üstünde kalır; yalnız “g”, “j” gibi harflerin kuyrukları bu çizginin altına iner.

Sık yapılan hata

drawString(metin, x, 0) yazıp metni panelin en üstüne koymayı beklemek. y = 0 taban çizgisi olduğu için harflerin neredeyse tamamı panelin dışında, görünmez bölgede kalır. Metnin görünmesi için y en az yazı tipi yüksekliği kadar olmalıdır; 24 puntoluk bir yazı için 30–40 civarı güvenli bir değerdir.

9.1.7Graphics çizim metotları

Graphics sınıfının sık kullanılan metotları Tablo 9.2'de toplanmıştır. Adlarda bir düzen vardır: draw… ile başlayan metotlar yalnız çerçeveyi, fill… ile başlayanlar içi dolu şekli çizer.

Tablo 9.2: Graphics sınıfının temel çizim metotları.
MetotNe yapar?
drawLine(x1, y1, x2, y2)İki nokta arasında çizgi
drawRect(x, y, en, boy)Dikdörtgen çerçevesi; (x, y) sol üst köşe
fillRect(x, y, en, boy)İçi dolu dikdörtgen
drawOval(x, y, en, boy)Bu dikdörtgene sığan elips (en = boy ise çember)
fillOval(x, y, en, boy)İçi dolu elips ya da daire
drawString(s, x, y)Metin; y taban çizgisi
setColor(c) / setFont(f)Sonraki çizimlerin rengi ya da yazı tipi
drawImage(resim, x, y, null)Bir resmi (x, y) noktasına çizer

Elips ve daireler merkezleriyle değil, sınırlayıcı dikdörtgenleriyle verilir. Elipsin sığdığı dikdörtgenin sol üst köşesi ve boyutu yazılır.

Tanım 9.5

Bir şeklin sınırlayıcı dikdörtgeni (bounding rectangle), şekli tam içine alan en küçük, kenarları eksenlere paralel dikdörtgendir. drawOval(x, y, en, boy) elipsi, sol üst köşesi (x, y) ve boyutu en × boy olan dikdörtgenin içine çizer.

(40, 40)genişlik 16090
Şekil 9.6: drawOval(40, 40, 160, 90). Elips, sol üst köşesi (40, 40) olan 160 × 90'lık dikdörtgene sığar.

Merkezi (cx, cy), yarıçapı r olan bir daire çizmek için sınırlayıcı karenin sol üst köşesi hesaplanır. Kare, merkezden sola ve yukarı r kadar uzanır; kenarı 2r'dir. Sonuç: fillOval(cx − r, cy − r, 2r, 2r). Tersine, verilen bir drawOval(x, y, en, boy) çağrısında elipsin merkezi (x + en / 2, y + boy / 2)'dir.

Örnek 9.2

g.drawOval(50, 50, 100, 60) ile çizilen elipsin merkezi neresidir? Elips hangi piksel aralığını kaplar?

Merkez (50 + 100 / 2, 50 + 60 / 2) = (100, 80)'dir. Elips yatayda 50'den, dikeyde 50'den başlar. Bir resim üzerinde çizilip boyalı pikseller sayıldığında x 50 ile 150, y 50 ile 110 arasında çıkar. Çerçeve çizen metotlar, sınırın sağ ve alt kenarını da boyar: draw… şekli en + 1 piksel geniş ve boy + 1 piksel yüksektir. fill… ise tam en × boy piksel boyar.

Son cümledeki fark küçük ama gerçektir. drawRect(20, 30, 50, 40) çerçevenin sağ alt köşe pikseli (70, 70)'tir; (71, 70) boş kalır. Aynı argümanlarla fillRect ise (69, 69)'a kadar boyar. Bir çerçeveyi dolu bir dikdörtgenin üstüne çizerken bu bir piksellik fark görünür hâle gelir.

9.1.8Kod izleme: şekillerin çizim sırası

Aşağıdaki panel, temel metotların her birini bir kez kullanır.

class SekillerPaneli extends JPanel {
    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.drawLine(10, 10, 110, 60);
        g.drawRect(130, 10, 80, 50);
        g.fillRect(230, 10, 80, 50);
        g.drawOval(10, 80, 100, 60);
        g.fillOval(130, 80, 60, 60);
        g.drawString("Merhaba", 230, 115);
    }
}
Merhaba
Şekil 9.7: SekillerPaneli'nin çıktısı: çizgi, dikdörtgen çerçevesi, dolu dikdörtgen, elips, dolu daire ve metin.

Kodda hiç setColor çağrısı yoktur. Renk verilmediğinde kalem panelin ön plan rengiyle çizer. Swing'in varsayılan görünümünde bu renk (51, 51, 51) koyu gridir; çalıştırılan programda getForeground() bu değeri döndürür. Tablo 9.3 metodun adımlarını izler.

Tablo 9.3: SekillerPaneli.paintComponent izlemesi.
AdımSatırDeğişkenlerAçıklama
14: super.paintComponent(g);g=Graphics (koyu gri kalem)Swing bu metodu kendisi çağırır. super.paintComponent(g) tuvali arka plan rengiyle temizler.
25: g.drawLine(10, 10, 110, 60);x1=10, y1=10, x2=110, y2=60İki nokta arasına çizgi. y aşağı doğru büyüdüğü için çizgi sağa ve aşağı iner.
36: g.drawRect(130, 10, 80, 50);x=130, y=10, en=80, boy=50drawRect: (x, y) sol üst köşe; yalnız çerçeve çizilir, içi boş kalır.
47: g.fillRect(230, 10, 80, 50);x=230, y=10, en=80, boy=50fillRect: aynı boyutta, içi kalemin rengiyle dolu dikdörtgen.
58: g.drawOval(10, 80, 100, 60);x=10, y=80, en=100, boy=60drawOval: kesikli kutuya sığan elips. (10, 80) elipsin değil kutunun köşesi.
69: g.fillOval(130, 80, 60, 60);x=130, y=80, en=60, boy=60En = boy olduğundan dolu bir daire: merkez (160, 110), yarıçap 30.
710: g.drawString("Merhaba", 230, 115);s="Merhaba", x=230, y=115drawString'de y, metnin taban çizgisidir: harfler 115'in üstüne yazılır.
811: }—Metot biter; pencere her yeniden boyandığında (boyut değişimi, repaint()) aynı sırayla baştan çizilir.

2. adımdaki çizgi (10, 10)'dan (110, 60)'a gider. İkinci noktanın y'si daha büyük olduğu için çizgi sağa ve aşağı iner. 3. ve 4. adımlar aynı boyutta iki dikdörtgen çizer. Biri yalnız çerçevedir, öbürü kalemin rengiyle doludur. 5. adımda (10, 80) noktası elipsin değil, sınırlayıcı kutunun köşesidir.

(0, 0)x →(10, 80)
Şekil 9.8: 5. adım. drawOval(10, 80, 100, 60): elips kesikli çizgiyle gösterilen sınırlayıcı kutuya sığar.

6. adımda en ve boy eşittir, bu yüzden dolu bir daire çizilir. Merkez (130 + 30, 80 + 30) = (160, 110), yarıçap 30'dur. 7. adımda metnin taban çizgisi y = 115'tir; harfler bu çizginin üstüne yazılır. Metot bittiğinde panel tamamlanmıştır. Pencere her yeniden boyandığında aynı çağrılar aynı sırayla baştan yapılır.

Çizimler üst üste boyanır. Sonradan çizilen şekil öncekini örter. Bu programda şekiller çakışmadığı için sıra görünmez. Şekiller çakıştığında ise son çizilen üstte kalır.

Sık yapılan hata

super.paintComponent(g) satırını unutmak. Bu satır zemini temizler. Yazılmazsa panel arka planıyla boyanmaz; önceki çizimden ya da başka pencerelerden kalan görüntü artıkları ekranda kalabilir. Özellikle hareketli çizimlerde şekil, geçtiği her yerde iz bırakır.

9.2Pencere ve panel

9.2.1JFrame sınıfı

Bir GUI programının bütün bileşenleri bir pencerenin içinde yaşar. Swing'de pencere JFrame sınıfıyla oluşturulur.

Tanım 9.6

JFrame, ekranda başlık çubuğu, simge durumuna küçültme ve kapatma düğmeleri olan üst düzey penceredir (top-level window). Başka bir bileşenin içine konmaz; diğer bileşenler onun içerik bölmesine eklenir.

Tablo 9.4: JFrame ile sık kullanılan çağrılar.
KodGörevi
JFrame pencere = new JFrame("Başlık");Başlığı verilen pencereyi oluşturur (henüz görünmez)
pencere.add(bilesen);Pencereye (içerik bölmesine) bileşen ekler
pencere.setSize(400, 300);Genişlik ve yükseklik (piksel, kenarlıklar dahil)
pencere.pack();Boyutu içerikteki bileşenlerin tercih ettiği boyuta göre ayarlar
pencere.setLocation(x, y);Pencerenin ekrandaki konumu; setLocationRelativeTo(null) ortalar
pencere.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);Kapatılınca programı sonlandırır
pencere.setVisible(true);Pencereyi görünür yapar (genellikle en son çağrılır)

Pencere boyutunun iki yolu vardır. setSize boyutu doğrudan verir; bu boyut başlık çubuğunu ve kenarlıkları da içerir. pack() ise içerikteki bileşenlerin tercih ettiği boyutları toplar ve pencereyi tam o kadar büyütür. Bileşenleri yerleşim yöneticisiyle dizilmiş bir pencerede pack() genellikle daha iyi sonuç verir, çünkü yazı tipi ve ekran çözünürlüğü değişse de içerik pencereye sığar.

add çağrısı bileşeni aslında pencerenin kendisine değil, içerik bölmesine (content pane) ekler. İçerik bölmesi, başlık çubuğunun altında kalan iç alandır. Eski Java sürümlerinde pencere.getContentPane().add(...) yazmak zorunluydu. Java 5'ten beri pencere.add(...) bu çağrıyı kendisi yapar. Bu nottaki örnekler iki yazımı da kullanır; anlamları aynıdır.

9.2.2İlk pencere

import javax.swing.JFrame;

public class JFrameOrnegi {
    public static void main(String[] args) {
        JFrame pencere = new JFrame("Java JFrame Örneği");
        pencere.setSize(400, 300);
        pencere.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        pencere.setVisible(true);
    }
}

Program yalnız JFrame sınıfını içe aktarır. main başlığı “Java JFrame Örneği” olan bir pencere oluşturur, boyutunu 400 × 300 yapar, kapatma davranışını belirler ve pencereyi görünür kılar. Pencerenin içi boştur. main bu dört satırdan sonra biter, ama program pencere kapanana kadar çalışmayı sürdürür.

Bu kısa programda üç tuzak vardır:

  • setVisible(true) unutulursa pencere bellekte oluşur ama ekranda görünmez. Program hata vermez; yalnız hiçbir şey olmuyormuş gibi görünür.
  • EXIT_ON_CLOSE verilmezse varsayılan davranış HIDE_ON_CLOSE'dur. Kullanıcı × düğmesine basınca pencere gizlenir, ama program arka planda çalışmaya devam eder. Geliştirme ortamında bu, durdurulması unutulan program örnekleri olarak birikir.
  • Boyut verilmezse pencere 0 × 0'a yakın, yalnız başlık çubuğu kadar açılır.

setDefaultCloseOperation'ın kabul ettiği dört sabit WindowConstants arayüzünde tanımlıdır. DO_NOTHING_ON_CLOSE (0) kapatmayı tümüyle engeller. HIDE_ON_CLOSE (1) pencereyi gizler. DISPOSE_ON_CLOSE (2) pencereyi yok eder; başka pencere kalmamışsa program bitebilir. EXIT_ON_CLOSE (3) System.exit(0) çağırır.

Bu program, ekranı olmayan bir ortamda çalıştırılırsa ilginç bir sonuç verir. Java, ekransız ortamı “başsız” (headless) kip olarak tanır. Bu kipte new JFrame(...) çağrısı java.awt.HeadlessException fırlatır, çünkü gösterilecek ekran yoktur. Panel, düğme ve metin alanı gibi bileşenler ise bu kipte de oluşturulabilir. Notta verilen çıktıların çoğu bu yolla, pencere açılmadan, bileşenleri doğrudan çalıştırarak elde edilmiştir.

9.2.3JPanel sınıfı

Tanım 9.7

Kapsayıcı (container), içinde başka bileşenler barındıran bileşendir. JPanel, javax.swing paketindeki genel amaçlı kapsayıcıdır; pencere içindeki bileşenleri gruplamak, düzenlemek ve üzerine çizim yapmak için kullanılır.

JPanel'in dört kullanımı vardır:

  • Gruplamak: Birden fazla panel iç içe konarak karmaşık düzenler kurulur. Örneğin bir pencerenin altındaki düğme sırası ayrı bir panelde toplanır.
  • Özelleştirmek: setBackground(), setBorder() ve setPreferredSize() ile rengi, kenarlığı ve tercih edilen boyutu değiştirilir.
  • Tuval olmak: Çizim, animasyon ve grafikler panelde yapılır: JPanel'den türetilir ve paintComponent geçersiz kılınır.
  • Kodu bölmek: Her panel ayrı bir sınıf olabilir. Büyük bir arayüz böylece küçük, ayrı ayrı anlaşılabilen parçalara bölünür.
JFramepencereiçerik bölmesigetContentPane()JPanelpanelJButtondüğmeJLabeletiketJPaneliç panel
Şekil 9.9: İç içe bileşenler. JFrame içerik bölmesini, içerik bölmesi bir JPanel'i, bu panel de bir düğme, bir etiket ve başka bir paneli içerir.

Şekil 9.9'daki yapı bir ağaçtır. Kök pencerededir, yapraklar düğme ve etiket gibi bileşenlerdir. Ara düğümler kapsayıcılardır. Bir kapsayıcı yeniden boyanırken önce kendisini, sonra içindeki bileşenleri çizer. Bileşen ağacının her düzeyinde aynı kural işler.

9.2.4Panel örneği

import java.awt.Color;
import javax.swing.*;

public class PanelOrnegi {
    public static void main(String[] args) {
        JPanel panel = new JPanel();
        panel.setBackground(Color.LIGHT_GRAY);
        panel.add(new JButton("Tıkla"));
        JFrame pencere = new JFrame("JPanel Örneği");
        pencere.add(panel);
        pencere.setSize(300, 150);
        pencere.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        pencere.setVisible(true);
    }
}

Program bir panel oluşturur ve arka planını Color.LIGHT_GRAY, yani (192, 192, 192) açık gri yapar. Panele “Tıkla” yazılı bir düğme eklenir. Ardından pencere oluşturulur, panel pencereye eklenir ve pencere 300 × 150 boyutunda gösterilir. Düğmenin bir dinleyicisi yoktur; tıklanınca görsel olarak basılır, ama hiçbir şey olmaz.

Pencerede düğme panelin üst ortasında durur. Panel ise bütün pencereyi kaplar. Bu iki gözlemin nedeni yerleşim yöneticileridir. JPanel'in varsayılan yerleşim yöneticisi FlowLayout'tur. Bu yönetici bileşenleri tercih ettikleri boyutta, satır satır ve ortalı dizer. Panelin içinde tek bir düğme olduğu için düğme ilk satırın ortasına konur. JFrame'in içerik bölmesi ise BorderLayout kullanır. Bölge belirtilmeden eklenen panel merkez bölgeye yerleşir. Merkez de geriye kalan bütün alanı alır.

Örnek 9.3

Bu panel 284 × 111 piksel olsun. “Tıkla” düğmesinin tercih ettiği boyut 61 × 26 ise düğmenin konumu ne olur?

FlowLayout varsayılan olarak bileşenler arasında ve kenarlarda 5 piksel boşluk bırakır. Kullanılabilir genişlik 284 − 2 × 5 = 274'tür. Düğme bu genişliğin ortasına konur: x = 5 + (274 − 61) / 2 = 5 + 106 = 111. Satır üst kenardan 5 piksel aşağıda başlar: y = 5. Başsız kipte panel bu boyuta getirilip düzen hesaplatıldığında düğmenin sınırları [x=111, y=5, width=61, height=26] çıkar. Düğmenin boyutu yazı tipine bağlıdır; başka bir sistemde birkaç piksel farklı olabilir.

9.3Çizim araçları

Çizimde üç küçük sınıf sürekli kullanılır: bir noktayı tutan Point, rengi tutan Color ve yazı tipini tutan Font. Üçü de java.awt paketindedir.

9.3.1Point sınıfı

Tanım 9.8

java.awt.Point, ekrandaki bir (x, y) noktasını tutan sınıftır. x ve y alanları public int'tir. getX() ve getY() aynı değerleri double olarak döndürür.

Point, sarmalama ilkesinin bilinçli bir istisnasıdır: alanları public'tir ve doğrudan okunup yazılabilir. Grafik kodunda nokta koordinatlarına çok sık erişildiği için kütüphane sadeliği tercih etmiştir. Sınıfın metotları da vardır. Aşağıdaki kod bunların çoğunu kullanır.

Point nokta = new Point(30, 40);
double xKoordinati = nokta.getX();
double yKoordinati = nokta.getY();
System.out.println(xKoordinati + " " + yKoordinati);
System.out.println(nokta.x + " " + nokta.y);
System.out.println(nokta.distance(0, 0));
nokta.setLocation(60, 80);
System.out.println(nokta);
nokta.translate(5, -10);   // 5 sağa, 10 yukarı
System.out.println(nokta);
System.out.println(Point.distance(0, 0, 3, 4));
30.0 40.0
30 40
50.0
java.awt.Point[x=60,y=80]
java.awt.Point[x=65,y=70]
5.0
Tablo 9.5: Point örneğinin izlemesi.
AdımSatırDeğişkenlerYeni çıktı
11: Point nokta = new Point(30, 40);nokta=(30, 40)—
22: double xKoordinati = nokta.getX();nokta=(30, 40), xKoordinati=30.0—
33: double yKoordinati = nokta.getY();nokta=(30, 40), xKoordinati=30.0, yKoordinati=40.0—
44: System.out.println(xKoordinati + " " + yKoordinati);nokta=(30, 40), xKoordinati=30.0, yKoordinati=40.030.0 40.0
55: System.out.println(nokta.x + " " + nokta.y);nokta=(30, 40), xKoordinati=30.0, yKoordinati=40.030 40
66: System.out.println(nokta.distance(0, 0));nokta=(30, 40), xKoordinati=30.0, yKoordinati=40.050.0
77: nokta.setLocation(60, 80);nokta=(60, 80), xKoordinati=30.0, yKoordinati=40.0—
88: System.out.println(nokta);nokta=(60, 80), xKoordinati=30.0, yKoordinati=40.0java.awt.Point[x=60,y=80]
99: nokta.translate(5, -10); // 5 sağa, 10 yukarınokta=(65, 70), xKoordinati=30.0, yKoordinati=40.0—
1010: System.out.println(nokta);nokta=(65, 70), xKoordinati=30.0, yKoordinati=40.0java.awt.Point[x=65,y=70]
1111: System.out.println(Point.distance(0, 0, 3, 4));nokta=(65, 70), xKoordinati=30.0, yKoordinati=40.05.0

1. adımda (30, 40) noktası oluşur. 2. ve 3. adımlarda getX() ve getY() değerleri double değişkenlere alınır. 4. adım bu değerleri yazar: 30.0 40.0. Ondalık nokta, değerin double olduğunu gösterir. 5. adım alanlara doğrudan erişir; nokta.x bir int olduğu için 30 40 yazılır. Aynı sayının iki farklı biçimde yazılması, tipin çıktıyı nasıl etkilediğini gösterir.

6. adımda distance(0, 0) noktanın orijine uzaklığını hesaplar. Pisagor bağıntısıyla √(30² + 40²) = √2500 = 50.0 bulunur. 7. adımda setLocation(60, 80) noktayı yeni bir konuma taşır. xKoordinati ve yKoordinati değişmez. Onlar 2. ve 3. adımda alınmış kopyalardır; nesneyle bağları kalmamıştır. 8. adımda println(nokta) nesnenin toString() metodunu çağırır ve sınıf adıyla alanları yazar: java.awt.Point[x=60,y=80].

(0, 0)(65, 70)
Şekil 9.10: 9. adım. translate(5, -10) noktayı 5 piksel sağa, 10 piksel yukarı taşır: (60, 80)'den (65, 70)'e.

9. adımdaki translate(dx, dy) göreli bir kaydırmadır: x'e dx, y'ye dy eklenir. dy = −10 olduğu için y 80'den 70'e azalır ve nokta ekranda yukarı çıkar (Şekil 9.10). Son satırdaki Point.distance(0, 0, 3, 4) bir sınıf metodudur. Nesne gerektirmez; iki noktanın koordinatlarını alır ve 5.0 döndürür.

Örnek 9.4

Point p = new Point(10, 20); Point q = p; q.translate(5, 5); p.setLocation(p.x * 2, p.y); satırlarından sonra p, q == p ve q.distance(30, 29) nedir?

Point q = p; yeni bir nokta oluşturmaz. İki değişken aynı nesneyi gösterir. q.translate(5, 5) bu ortak nesneyi (15, 25)'e taşır. p.setLocation(15 * 2, 25) nesneyi (30, 25)'e taşır. p ve q aynı nesne olduğu için q == p true'dur. (30, 25) ile (30, 29) arası dikey bir doğrudur: uzaklık 4.0. Bağımsız bir kopya isteniyorsa new Point(p) yazılır.

java.awt.Point[x=30,y=25] true
4.0
140 70 35

Çıktının üçüncü satırı bir sonraki sınıfla ilgilidir: new Color(200, 100, 50).darker() rengin bileşenlerini verir.

9.3.2Color sınıfı

java.awt.Color, arayüzün ve çizimlerin rengini tutar. Ekrandaki her renk, üç ana rengin karışımıdır: kırmızı (red), yeşil (green) ve mavi (blue). Bu düzene RGB denir.

Tanım 9.9

RGB rengi, kırmızı, yeşil ve mavi bileşenlerin her biri 0–255 aralığında bir tam sayı olan üçlüdür. new Color(int red, int green, int blue) bu üçlüden bir renk nesnesi oluşturur. (0, 0, 0) siyah, (255, 255, 255) beyazdır.

Rengi elde etmenin iki yolu vardır. Birincisi hazır sabitlerdir: Color.RED, Color.GREEN, Color.WHITE, Color.LIGHT_GRAY gibi. Bu sabitlerin küçük harfli eş adları da vardır (Color.red); ikisi aynı nesneyi gösterir. İkincisi RGB üçlüsüyle yeni bir nesne oluşturmaktır. 0–255 dışındaki bir değer IllegalArgumentException fırlatır; new Color(300, 0, 0) çağrısının iletisi Color parameter outside of expected range: Red'dir.

Color turuncu = new Color(255, 128, 0);
System.out.println(turuncu);
System.out.println(turuncu.getGreen());
System.out.println(Color.RED.darker());
java.awt.Color[r=255,g=128,b=0]
128
java.awt.Color[r=178,g=0,b=0]

Bir rengin koyu ve açık tonları için iki metot vardır. darker() her bileşeni 0,7 ile çarpar ve tam sayıya keser. brighter() ise her bileşeni 0,7'ye böler, sonucu 255'te sınırlar. İki metot da asıl rengi değiştirmez; yeni bir Color nesnesi döndürür. Color nesneleri değiştirilemezdir.

Tablo 9.6: Bazı renkler ve RGB bileşenleri (çalıştırılarak doğrulandı).
İfadeRGBHesap
Color.RED(255, 0, 0)hazır sabit
Color.BLUE(0, 0, 255)hazır sabit
Color.LIGHT_GRAY(192, 192, 192)hazır sabit
new Color(255, 128, 0)(255, 128, 0)turuncu
turuncu.darker()(178, 89, 0)255 × 0,7 = 178,5 → 178; 128 × 0,7 = 89,6 → 89
Color.RED.darker()(178, 0, 0)0 × 0,7 = 0
Color.GREEN.darker()(0, 178, 0)giriş yasak işaretinin zemini
new Color(100, 50, 0).brighter()(142, 71, 0)100 / 0,7 = 142,8 → 142; 50 / 0,7 = 71,4 → 71
Color.RED.brighter()(255, 0, 0)255 zaten sınırda; 0 bileşenler 0 kalır
Color.RED.darker().brighter()(254, 0, 0)178 / 0,7 = 254,2 → 254: kesme kaybı

Tablonun son satırı bir uyarı taşır. darker() ve brighter() birbirinin tam tersi değildir. Her işlem sonucu tam sayıya keser ve kesilen kesir geri gelmez. Kırmızıyı koyultup yeniden açınca (255, 0, 0) değil, (254, 0, 0) elde edilir.

brighter()'ın sıfır bileşenleri neden değiştirmediği de sorulabilir. Sıfırın 0,7'ye bölümü yine sıfırdır. Bu yüzden saf kırmızı “açılınca” pembeye dönmez, aynı kalır. Kütüphane tek bir özel durumu ayrıca ele alır: siyah (0, 0, 0) açılınca (3, 3, 3) olur, çünkü aksi hâlde siyah hiç açılamazdı.

Örnek 9.5

new Color(200, 100, 50).darker() hangi bileşenleri verir?

Her bileşen 0,7 ile çarpılıp kesilir: 200 × 0,7 = 140, 100 × 0,7 = 70, 50 × 0,7 = 35. Sonuç (140, 70, 35)'tir. Bu değerler bir önceki örneğin çıktısının üçüncü satırıyla aynıdır.

9.3.3Font sınıfı

java.awt.Font, metnin yazı tipini, stilini ve boyutunu belirler. Yapıcı üç parametre alır: new Font(ad, stil, punto).

Tablo 9.7: Font yapıcısının parametreleri.
ParametreDeğerler
ad"Serif", "SansSerif", "Monospaced", "Dialog" (her sistemde bulunan mantıksal adlar) ya da "Arial" gibi gerçek bir yazı tipi
stilFont.PLAIN (0), Font.BOLD (1), Font.ITALIC (2); ikisi birden: Font.BOLD | Font.ITALIC
puntoTam sayı boyut, örneğin 24

Mantıksal ad, Java'nın her sistemde gerçek bir yazı tipine eşlediği addır. "Serif" Windows'ta çoğunlukla Times New Roman'a, "Monospaced" Courier New'e karşılık gelir. Gerçek bir yazı tipi adı verilir ve o yazı tipi sistemde yoksa Java "Dialog"'a döner. Taşınabilir programlar bu yüzden mantıksal adları tercih eder.

Stil sabitleri bit bayraklarıdır. BOLD sayının 1. bitini, ITALIC 2. bitini kullanır. | işleci (bit düzeyinde VEYA) iki bayrağı birleştirir: 1 | 2 = 3. new Font("Serif", Font.BOLD | Font.ITALIC, 20) nesnesinde getStyle() 3 döndürür; isBold() ve isItalic() ikisi de true'dur.

public void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.setColor(new Color(0, 102, 204));
    g.setFont(new Font("Serif", Font.BOLD, 24));
    g.drawString("Java", 20, 40);
    g.setFont(new Font("Monospaced", Font.PLAIN, 14));
    g.drawString("Grafik", 20, 70);
}

Metot kalemi önce (0, 102, 204) mavisine çevirir. İlk metin kalın, 24 puntoluk Serif ile (20, 40)'a yazılır. Ardından yazı tipi değiştirilir ve ikinci metin 14 puntoluk Monospaced ile (20, 70)'e yazılır. Renk değiştirilmediği için iki metin de aynı mavidedir.

JavaGrafik
Şekil 9.11: YaziPaneli'nin çıktısı: kalın Serif 24 punto “Java” ve Monospaced 14 punto “Grafik”.

JLabel gibi bileşenlerde de aynı Font nesnesi kullanılır: etiket.setFont(f). Var olan bir yazı tipinden yeni bir tane türetmek için deriveFont metodu vardır. f.deriveFont(Font.ITALIC, 18f) aynı ailede, italik ve 18 puntoluk yeni bir nesne döndürür. f değişmez.

Sık yapılan hata

Yalnız boyutu değiştirmek için f.deriveFont(18) yazmak. deriveFont'un tek int parametreli sürümü boyutu değil stili değiştirir. 24 puntoluk kalın bir yazı tipinde bu çağrı 18'i stil olarak yorumlar ve boyutu 24'te bırakır. Denendiğinde sonuç stil 0, boyut 24'tür. Boyut için float sürümü kullanılır: f.deriveFont(18f).

9.4Yerleşim yöneticileri

9.4.1Yerleşim yöneticisi nedir?

Bir pencereye beş düğme konacağını düşünelim. Her düğmenin konumu ve boyutu elle verilebilir: “birinci düğme (10, 10)'da, 80 × 30”. Bu yöntem bir sorunla karşılaşır. Kullanıcı pencereyi büyütünce düğmeler sol üstte kalır. Ekran çözünürlüğü ya da yazı tipi değişince düğme metinleri kutulara sığmaz. Java bu işi bir nesneye devreder.

Tanım 9.10

Yerleşim yöneticisi (layout manager), bir kapsayıcının içindeki bileşenlerin konumunu ve boyutunu kurallara göre hesaplayan nesnedir. Kapsayıcıya setLayout ile verilir: panel.setLayout(new GridLayout(2, 3));.

Yerleşim yöneticisi koordinatları elle vermenin yerine kullanılır. Pencere büyüyünce, yazı tipi ya da ekran çözünürlüğü değişince düzen kendiliğinden yeniden hesaplanır. Programcı yalnız kuralı seçer: “satır satır diz”, “beş bölgeye yerleştir”, “eşit hücrelere böl”. Java'nın standart yöneticilerinden dördü bu notta işlenir.

1234 NSWECENTER 123456 123
Şekil 9.12: Dört yerleşim yöneticisi. Soldan sağa: FlowLayout (soldan sağa, satır dolunca alta; JPanel varsayılanı), BorderLayout (beş bölge; JFrame içerik bölmesi varsayılanı), GridLayout(2, 3) (eşit boyutlu hücreler), BoxLayout.Y_AXIS (tek sütun).

Daha esnek ama karmaşık olan GridBagLayout, farklı boyutlarda hücrelerden oluşan bir ızgara kurar. Çoğu zaman buna gerek kalmaz. Panelleri iç içe koyup her birine basit bir yönetici vermek yeterlidir. Örneğin bir pencerenin içerik bölmesi BorderLayout kullanır, alttaki düğme şeridi ise FlowLayout kullanan ayrı bir paneldir.

Yerleşim yöneticilerinin davranışı etkileşimli olarak Swing Yerleşim Yöneticileri simülatöründe izlenebilir. Simülatörde pencere boyutu değiştikçe dört yöneticinin düzeni nasıl yeniden hesapladığı görülür.

9.4.2Kod izleme: FlowLayout satırları nasıl doldurur?

Yerleşim yöneticisinin çalışmasını anlamanın en iyi yolu, bir düzeni elle hesaplamaktır. Aşağıdaki kod, 260 piksel genişliğindeki bir panele genişlikleri farklı beş düğme ekler ve hesaplanan konumları yazar.

int[] gen = {90, 60, 110, 70, 80};
JPanel panel = new JPanel();   // FlowLayout(CENTER, 5, 5)
panel.setSize(260, 120);
for (int i = 0; i < gen.length; i++) {
    JButton b = new JButton("B" + i);
    b.setPreferredSize(new Dimension(gen[i], 30));
    panel.add(b);
}
panel.doLayout();              // konumları hesapla
for (Component c : panel.getComponents())
    System.out.println(c.getX() + ", " + c.getY());
52, 5
147, 5
37, 40
152, 40
90, 75

FlowLayout'un varsayılan ayarları FlowLayout(CENTER, 5, 5)'tir: satırlar ortalanır, bileşenler arasında yatayda ve dikeyde 5 piksel boşluk kalır. Aynı boşluk panelin kenarlarında da bırakılır. Her düğmenin tercih ettiği boyut setPreferredSize ile verilmiştir: genişlikler 90, 60, 110, 70 ve 80; yükseklikler 30.

Tablo 9.8: FlowLayout yerleşim izlemesi.
AdımSatırDeğişkenlerYeni çıktıAçıklama
11: int[] gen = {90, 60, 110, 70, 80};
2: JPanel panel = new JPanel(); // FlowLayout(CENTER, 5, 5)
3: panel.setSize(260, 120);
gen=[90, 60, 110, 70, 80], panel=260×120—JPanel'in varsayılan yöneticisi FlowLayout: ortalı, bileşenler arası 5 piksel boşluk. Kesikli çizgiler kenar boşluğu.
25: JButton b = new JButton("B" + i);
6: b.setPreferredSize(new Dimension(gen[i], 30));
7: panel.add(b);
i=0, b=B0 (90×30)—add düğmeyi yalnızca listeye ekler; konumu henüz (0, 0), boyutu 0 × 0.
34: for (int i = 0; i < gen.length; i++) {
5: JButton b = new JButton("B" + i);
6: b.setPreferredSize(new Dimension(gen[i], 30));
7: panel.add(b);
i=4, b=B4 (80×30)—Döngü bitti: beş düğme eklendi, hiçbiri yerleşmedi. Yerleşim ancak düzen hesaplanınca yapılır.
49: panel.doLayout(); // konumları hesaplasatır=1, genişlik=90+5+60=155—doLayout(): kullanılabilir genişlik 260 − 2 × 5 = 250. B0 ve B1 sığar: 155 piksel.
59: panel.doLayout(); // konumları hesaplasatır=1, deneme=155+5+110 = 270 > 250—B2 de eklenirse satır 270 piksel olur: sığmaz, alt satıra iner.
69: panel.doLayout(); // konumları hesaplasatır=1, x0=5+(250−155)/2 = 52—Satır 1 ortalanır: B0 x = 52, B1 x = 52 + 90 + 5 = 147, y = 5.
79: panel.doLayout(); // konumları hesaplasatır=2, genişlik=110+5+70 = 185—Satır 2: B2 ve B3 (185 piksel); B4 eklense 270 olur. Ortalama: x0 = 5 + (250 − 185)/2 = 37.
89: panel.doLayout(); // konumları hesaplasatır=3, x0=5+(250−80)/2 = 90—B4 tek başına üçüncü satırda; y = 5 + 30 + 5 + 30 + 5 = 75.
910: for (Component c : panel.getComponents())
11: System.out.println(c.getX() + ", " + c.getY());
c=B0 … B452, 5 ⏎ 147, 5 ⏎ 37, 40 ⏎ 152, 40 ⏎ 90, 75Hesaplanan sınırlar yazdırılır. Panel genişlerse satırlar yeniden hesaplanır ve düğmeler yer değiştirir.

İzlemenin ilk dersi 2. ve 3. adımlardadır. add düğmeyi kapsayıcının bileşen listesine ekler, ama yerleştirmez. Döngü bittiğinde beş düğmenin de konumu (0, 0), boyutu 0 × 0'dır. Yerleşim ancak düzen hesaplanınca yapılır. Gerçek bir pencerede bu iş pack(), setVisible(true) ya da pencere boyutunun değişmesiyle kendiliğinden tetiklenir. Örnekte ise doLayout() açıkça çağrılır.

doLayout() satırları şöyle doldurur. Kullanılabilir genişlik, panel genişliğinden iki kenar boşluğunun çıkarılmasıyla bulunur: 260 − 2 × 5 = 250. Düğmeler sırayla satıra eklenir. B0 (90) ve B1 (60) aralarındaki 5 piksel boşlukla 155 piksel tutar; sığar. B2 de eklense satır 155 + 5 + 110 = 270 piksel olurdu. 270 > 250 olduğu için B2 alt satıra iner.

Bir satır kapanınca ortalanır. Satırın boşta kalan genişliği iki yana eşit dağıtılır: x0 = 5 + (250 − 155) / 2 = 5 + 47 = 52. Tam sayı bölmesi 47,5'i 47'ye keser. B0 x = 52'ye, B1 x = 52 + 90 + 5 = 147'ye konur; ikisinin y'si 5'tir.

panel 260 × 120B0B1B2B3B4eklendi:B0 90B1 60B2 110B3 70B4 80
Şekil 9.13: 8. adım. Üç satır kuruldu; B4 tek başına üçüncü satırda ortalanmış.

İkinci satır B2 (110) ve B3 (70) ile 185 piksel tutar. B4 eklense 185 + 5 + 80 = 270 olurdu; yine sığmaz. Ortalama: x0 = 5 + (250 − 185) / 2 = 5 + 32 = 37. B2 x = 37'ye, B3 x = 37 + 110 + 5 = 152'ye konur. Satırın y'si, önceki satırın altı artı boşluktur: 5 + 30 + 5 = 40. B4 üçüncü satırda yalnızdır: x0 = 5 + (250 − 80) / 2 = 90, y = 5 + 30 + 5 + 30 + 5 = 75. Hesap çıktıyla birebir tutar.

Panel genişlerse satırlar yeniden hesaplanır. Aynı panel 400 piksele çıkarılıp düzen yeniden hesaplatıldığında kullanılabilir genişlik 390 olur. İlk dört düğme (90 + 60 + 110 + 70 + 3 × 5 = 345) tek satıra sığar; B4 ikinci satıra iner:

(27, 5) (122, 5) (187, 5) (302, 5) (160, 40)
Örnek 9.6

200 piksel genişliğindeki bir FlowLayout paneline 80 × 30 boyutunda üç bileşen eklenir. Bileşenlerin konumları nedir?

Kullanılabilir genişlik 200 − 10 = 190'dır. İlk iki bileşen 80 + 5 + 80 = 165 piksel tutar; üçüncüsü eklense 250 olur, sığmaz. Birinci satırın başlangıcı 5 + (190 − 165) / 2 = 5 + 12 = 17'dir. Bileşenler (17, 5) ve (102, 5)'e konur. Üçüncü bileşen ikinci satırdadır: x = 5 + (190 − 80) / 2 = 60, y = 40. Başsız kipte çalıştırılan program aynı değerleri verdi.

9.4.3BorderLayout

BorderLayout, kapsayıcıyı beş bölgeye ayırır: NORTH (kuzey), SOUTH (güney), WEST (batı), EAST (doğu) ve CENTER (merkez). Önceliği farklı bileşenleri düzenli göstermek için uygundur: araç çubuğu üstte, durum satırı altta, asıl içerik ortada.

NORTH (kuzey)SOUTH (güney)WEST(batı)EAST(doğu)CENTER(merkez, kalan alan)
Şekil 9.14: BorderLayout bölgeleri.

Yöneticinin kuralları şunlardır:

  • Bölge belirtilmeden eklenen bileşen merkeze yerleşir: add(b) ile add(b, BorderLayout.CENTER) aynıdır.
  • Her bölgede tek bileşen olur. Aynı bölgeye ikinci ekleme öncekinin yerini alır.
  • Kuzey ve güney bütün genişliği alır; yükseklikleri bileşenin tercih ettiği yüksekliktir. Batı ve doğu, kuzeyle güney arasında kalan yüksekliği alır; genişlikleri tercih edilen genişliktir. Merkez geriye kalan her şeyi doldurur.
  • setHgap() ve setVgap() bölgeler arasındaki yatay ve dikey boşluğu piksel olarak ayarlar. Aynı değerler yapıcıya da verilebilir: new BorderLayout(5, 5).

İkinci kuralın ince bir yanı vardır. Aynı bölgeye ikinci bileşen eklendiğinde birincisi kapsayıcıdan silinmez. Kapsayıcının bileşen sayısı 2'dir; ama yönetici yalnız son ekleneni o bölgeye bağlar. Birinci bileşen yerleşime katılmaz ve boyutu 0 × 0 kalır. Ekranda görünmez.

Sık yapılan hata

Pencereye bölge belirtmeden iki bileşen eklemek: f.add(new JButton("Bir")); f.add(new JButton("İki"));. İçerik bölmesi BorderLayout kullandığı için ikisi de merkeze gider ve son eklenen kazanır. Ekranda yalnız “İki” görünür ve bütün alanı kaplar. 300 × 200'lük bir BorderLayout panelinde denendiğinde “İki” (0, 0, 300, 200), “Bir” ise 0 × 0 çıkar. Yan yana dizilim FlowLayout'un davranışıdır; bunun için düğmeler önce bir JPanel'e konur.

9.4.4Kod izleme: bölgeleri doldurmak

JPanel panel = new JPanel();
BorderLayout yerlesim = new BorderLayout();
yerlesim.setHgap(5);
yerlesim.setVgap(5);
panel.setLayout(yerlesim);
panel.add(new JButton("Kuzey"), BorderLayout.NORTH);
panel.add(new JButton("Güney"), BorderLayout.SOUTH);
panel.add(new JButton("Batı"), BorderLayout.WEST);
panel.add(new JButton("Doğu"), BorderLayout.EAST);
panel.add(new JButton("Merkez"), BorderLayout.CENTER);

Kod bir panel ve bir BorderLayout nesnesi oluşturur, boşlukları 5 piksel yapar ve yöneticiyi panele verir. Ardından beş bölgenin her birine bir düğme eklenir. Tablo 9.9 her eklemenin düzene etkisini anlatır.

Tablo 9.9: BorderLayout bölgelerinin dolması.
AdımSatırAçıklama
11: JPanel panel = new JPanel();
2: BorderLayout yerlesim = new BorderLayout();
3: yerlesim.setHgap(5);
4: yerlesim.setVgap(5);
5: panel.setLayout(yerlesim);
Panel BorderLayout kullanacak; bölgeler arasında 5 piksel boşluk var. Henüz bileşen yok.
26: panel.add(new JButton("Kuzey"), BorderLayout.NORTH);Kuzey düğmesi tüm genişliği alır, yüksekliği düğmenin tercih ettiği kadardır.
37: panel.add(new JButton("Güney"), BorderLayout.SOUTH);Güney de tüm genişlikte, en alta yerleşir.
48: panel.add(new JButton("Batı"), BorderLayout.WEST);Batı, kuzey ile güney arasında kalan yüksekliği alır; genişliği metne göre.
59: panel.add(new JButton("Doğu"), BorderLayout.EAST);Doğu sağ kenara yerleşir.
610: panel.add(new JButton("Merkez"), BorderLayout.CENTER);Merkez geriye kalan bütün alanı doldurur. Pencere büyütülürse en çok merkez büyür.
KuzeyGüneyBatıDoğuMerkez
Şekil 9.15: 6. adım. Beş bölge dolu; merkez geriye kalan bütün alanı aldı.

Piksel değerleri panel boyutuna ve düğmelerin tercih ettiği boyuta bağlıdır. Panel 400 × 250 yapılıp düzen hesaplatıldığında değerler şöyle çıkar. Düğmeler varsayılan yazı tipinde 26 piksel yüksektir. Kuzey (0, 0)'da 400 × 26, güney (0, 224)'te 400 × 26'dır. Batı ve doğu kuzeyin altından 5 piksel boşlukla başlar: y = 26 + 5 = 31. Yükseklikleri 250 − 26 − 26 − 2 × 5 = 188'dir. Batı 56, doğu 63 piksel geniştir; bunlar “Batı” ve “Doğu” metinlerinin gerektirdiği genişliklerdir. Merkez x = 56 + 5 = 61'den başlar ve 400 − 56 − 63 − 2 × 5 = 271 piksel genişliğindedir. Merkezin tercih ettiği 76 piksel bu hesapta hiç kullanılmaz.

Pencere büyütülürse en çok merkez büyür. Kuzey ve güney yalnız genişler, batı ve doğu yalnız uzar. Bu özellik BorderLayout'u ana pencere düzeni için uygun kılar: metin alanı ya da çizim tuvali merkeze konur ve pencereyle birlikte büyür.

Örnek 9.7

300 × 200 piksellik ve boşluksuz bir BorderLayout paneline beş bileşen eklenir. Tercih edilen boyutlar şöyledir: kuzey 100 × 30, güney 100 × 20, batı 60 × 50, doğu 40 × 50, merkez 10 × 10. Ardından kuzeye 80 × 40 tercihli ikinci bir bileşen eklenir. Bileşenlerin sınırları nedir?

Kuzey bütün genişliği alır; yüksekliği tercih edilen 30'dur: (0, 0, 300, 30). Tercih edilen en olan 100 yok sayılır. Güney en alttadır: y = 200 − 20 = 180, yani (0, 180, 300, 20). Batı ile doğu aradaki 200 − 30 − 20 = 150 piksel yüksekliği alır: batı (0, 30, 60, 150), doğu (300 − 40, 30, 40, 150) = (260, 30, 40, 150). Merkez kalanı alır: (60, 30, 200, 150). İkinci kuzey bileşeni ilkinin yerini alır ve 40 piksel yüksektir. Batı, doğu ve merkez 10 piksel aşağı kayar ve kısalır: batı (0, 40, 60, 140), merkez (60, 40, 200, 140). Eski kuzey bileşeni panelde kalır ama boyutu güncellenmez. Başsız kipte çalıştırılan program bu değerlerin hepsini verdi.

9.4.5GridLayout ve BoxLayout

GridLayout(satır, sütun), kapsayıcıyı eşit boyutlu hücrelere böler. Bileşenler satır satır, soldan sağa yerleşir. Her bileşen hücresini tümüyle doldurur; tercih edilen boyut dikkate alınmaz.

Örnek 9.8

new GridLayout(2, 3) kullanan 300 × 120 piksellik bir panele sırayla beş düğme eklenir. Beşinci düğmenin konumu ve boyutu nedir?

Hücre genişliği 300 / 3 = 100, yüksekliği 120 / 2 = 60'tır. Düğme 1–3 üst satırı doldurur. Düğme 4 alt satırın ilk hücresine, düğme 5 ikinci hücresine yerleşir: (100, 60, 100, 60). Altıncı hücre boş kalır. Hücre boyutu bileşen sayısına göre değişmez; çalıştırılan program aynı sınırları verdi.

Satır ya da sütun sayısından biri 0 verilebilir. new GridLayout(0, 2) “iki sütun, satır sayısı gerektiği kadar” demektir. 200 × 150'lik bir panele beş düğme eklenirse üç satır oluşur. Hücreler 100 × 50 olur ve beşinci düğme (0, 100)'e konur. Bileşen sayısı satır × sütun sayısını aşarsa ve ikisi de sıfırdan farklıysa, Java satır sayısını korur ve sütun sayısını artırır. GridLayout(2, 3)'e yedi düğme eklenince ızgara 2 × 4 olur ve hücreler 75 piksele daralır.

BoxLayout bileşenleri tek satıra ya da tek sütuna dizer. Yapıcısı, yönettiği kapsayıcıyı da parametre olarak alır: panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)). Y_AXIS alt alta, X_AXIS yan yana dizer. Formlarda etiketleri ve alanları alt alta sıralamak için kullanılır.

Sık yapılan hata

Yerleşim yöneticisi tanımlı bir kapsayıcıda bileşenin konumunu setBounds ya da setLocation ile vermek. Yönetici bir sonraki düzen hesabında bu değerleri ezer; elle verilen konum kaybolur. Bileşenin boyutunu etkilemek için setPreferredSize kullanılır. Yönetici bu tercihi kuralına göre dikkate alır ya da almaz.

9.5Uygulama: giriş yasak işareti

9.5.1Hedef ve katmanlar

Bu uygulama, trafikteki “girişi olmayan yol” işaretini çizen bir panel yazar. İşaret, üst üste boyanan dört katmandan oluşur:

  1. Yeşil arka plan (setBackground ile).
  2. Beyaz büyük daire (dış halka).
  3. Kırmızı, biraz küçük daire.
  4. Ortada beyaz yatay bar.
Şekil 9.16: Giriş yasak işareti: yeşil zemin üzerinde beyaz halka, kırmızı daire ve ortada beyaz yatay bar.

Programın önemli bir tasarım kararı vardır: bütün ölçüler getWidth() ve getHeight()'tan hesaplanır. Sabit piksel değeri kullanılmaz. Pencere büyütülünce işaret de büyür ve her zaman ortada kalır. Çizim, panelin o anki boyutuna göre her seferinde baştan hesaplanır.

9.5.2paintComponent kodu

public class GirisYasaktirIsareti extends JPanel {
    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        var c = new Point(getWidth() / 2, getHeight() / 2); // merkez
        var r = Math.min(getWidth() / 2, getHeight() / 2) - 5; // yarıçap
        var r2 = (int) (r * 0.9);   // iç yarıçap
        var bw = (int) (r2 * 1.4);  // bar genişliği
        var bh = (int) (r2 * 0.35); // bar yüksekliği
        g.setColor(Color.WHITE);
        g.fillOval(c.x - r, c.y - r, 2 * r, 2 * r);
        g.setColor(Color.RED);
        g.fillOval(c.x - r2, c.y - r2, r2 * 2, r2 * 2);
        g.setColor(Color.WHITE);
        g.fillRect(c.x - bw / 2, c.y - bh / 2, bw, bh);
    }
}

Kod beş değişken hesaplar, sonra üç şekil çizer. Değişkenler var ile tanımlanmıştır. Java 10'dan beri var, yerel değişkenin tipini derleyicinin sağdaki ifadeden çıkarmasını sağlar. Burada c'nin tipi Point, diğerlerinin tipi int'tir. Klasik yazım şöyledir: Point c = new Point(…); int r = …;. var tipi ortadan kaldırmaz; tip derleme anında belirlenir ve sonradan değişmez.

Satırları sırayla okuyalım:

  • c: Panelin merkezi. Genişlik ve yükseklik ikiye bölünür. İki işlenen de int olduğu için tam sayı bölmesi yapılır.
  • r: Dış dairenin yarıçapı. Merkezden kısa kenara olan uzaklık seçilir (Math.min) ve 5 piksel pay bırakılır. Daire böylece panelin dışına taşmaz.
  • r2: İç dairenin yarıçapı, r'nin %90'ı. r * 0.9 bir double'dır; (int) dönüşümü ondalık kısmı atar.
  • bw ve bh: Barın genişliği ve yüksekliği, iç yarıçapın 1,4 ve 0,35 katı.

Çizim üç adımda yapılır. Önce kalem beyaza çevrilir ve dış daire çizilir. Sonra kalem kırmızıya çevrilir ve iç daire çizilir. Kırmızı daire beyazın üstüne boyanır; beyazdan geriye yalnız dışta ince bir halka kalır. Son olarak kalem yeniden beyaza çevrilir ve bar çizilir. Bar, sınırlayıcı dikdörtgenin sol üst köşesiyle verilir: merkezden yarı genişlik kadar sola, yarı yükseklik kadar yukarı.

Gerekli içe aktarmalar java.awt.* ve javax.swing.*'tır. Point, Graphics ve Color birincisinden, JPanel ikincisinden gelir.

9.5.3Kod izleme: işareti çizmek

Panel 384 × 261 piksel olsun. Bu boyut, 400 × 300'lük bir pencereden başlık çubuğu ve kenarlıklar düşülünce kalan iç alanın tipik değeridir. Gerçek değer işletim sistemine ve tema ayarlarına göre birkaç piksel değişir.

Tablo 9.10: Panel 384 × 261 iken paintComponent izlemesi.
AdımSatırDeğişkenlerAçıklama
11: super.paintComponent(g);getWidth()=384, getHeight()=261Panel 384 × 261 piksel. super.paintComponent zemini arka plan rengiyle (yeşil) boyar.
22: var c = new Point(getWidth() / 2, getHeight() / 2);getWidth()=384, getHeight()=261, c=(192, 130)Merkez: (384 / 2, 261 / 2) = (192, 130). Tam sayı bölmesi: 130,5 → 130.
33: var r = Math.min(getWidth() / 2, getHeight() / 2) - 5;getWidth()=384, getHeight()=261, c=(192, 130), r=125Yarıçap: min(192, 130) − 5 = 125. Kısa kenara göre seçilir, 5 piksel pay kalır.
44: var r2 = (int) (r * 0.9);getWidth()=384, getHeight()=261, c=(192, 130), r=125, r2=112İç yarıçap: (int) 112,5 = 112.
55: var bw = (int) (r2 * 1.4);getWidth()=384, getHeight()=261, c=(192, 130), r=125, r2=112, bw=156Bar genişliği: (int) (112 × 1,4) = (int) 156,8 = 156.
66: var bh = (int) (r2 * 0.35);getWidth()=384, getHeight()=261, c=(192, 130), r=125, r2=112, bw=156, bh=39Bar yüksekliği: (int) 39,2 = 39.
77: g.setColor(Color.WHITE);
8: g.fillOval(c.x - r, c.y - r, 2 * r, 2 * r);
getWidth()=384, getHeight()=261, c=(192, 130), r=125, r2=112, bw=156, bh=39fillOval(67, 5, 250, 250): beyaz daire, sınırlayıcı kare kesikli çizgide.
89: g.setColor(Color.RED);
10: g.fillOval(c.x - r2, c.y - r2, r2 * 2, r2 * 2);
getWidth()=384, getHeight()=261, c=(192, 130), r=125, r2=112, bw=156, bh=39fillOval(80, 18, 224, 224): kırmızı daire beyazın üstüne boyanır; 13 piksellik beyaz halka kalır.
911: g.setColor(Color.WHITE);
12: g.fillRect(c.x - bw / 2, c.y - bh / 2, bw, bh);
getWidth()=384, getHeight()=261, c=(192, 130), r=125, r2=112, bw=156, bh=39fillRect(114, 111, 156, 39): beyaz bar en üste. İşaret tamam.

2. adımda merkez (384 / 2, 261 / 2) hesaplanır. 261 / 2 matematikte 130,5'tir, ama tam sayı bölmesi ondalığı atar: (192, 130). 3. adımda yarıçap için min(192, 130) − 5 = 125 bulunur. Kısa kenar yükseklik olduğu için yarıçapı o belirler.

4.–6. adımlar tip dönüşümünü gösterir. 125 × 0,9 = 112,5; (int) bunu 112'ye keser. Yuvarlama yapılmaz: 112,5 bile aşağı iner. 112 × 1,4 ise 156,8 olmalıdır. double aritmetiği bu değeri tam gösteremez; Java'da çarpımın sonucu 156.79999999999998'dir. Kesme yine 156 verir. Bar yüksekliği 112 × 0,35 = 39,2; kesilince 39'dur.

Şekil 9.17: 8. adım. fillOval(80, 18, 224, 224): kırmızı daire beyazın üstüne boyandı; sınırlayıcı kare kesikli çizgiyle gösterilmiştir.

7. adımdaki fillOval(c.x − r, c.y − r, 2 * r, 2 * r) çağrısının argümanları (192 − 125, 130 − 125, 250, 250) = (67, 5, 250, 250)'dir. 8. adımda iç daire (192 − 112, 130 − 112, 224, 224) = (80, 18, 224, 224) ile çizilir. İki yarıçap arasındaki fark 125 − 112 = 13 piksellik beyaz halkayı verir. 9. adımda bar (192 − 78, 130 − 19, 156, 39) = (114, 111, 156, 39)'a çizilir. bw / 2 = 78 ve bh / 2 = 19 değerleri de tam sayı bölmesidir.

Hesaplanan çağrılar Java'da doğrulandı. Panel başsız kipte 384 × 261 boyutuna getirildi ve paintComponent bir resim üzerinde DebugGraphics kalemiyle çalıştırıldı. Kayıtta tam olarak fillOval(67, 5, 250, 250), fillOval(80, 18, 224, 224) ve fillRect(114, 111, 156, 39) görüldü. Resimdeki pikseller de beklenen renkleri taşıyordu: merkez beyaz (bar), (192, 25) kırmızı, (192, 10) beyaz (halka), (5, 5) yeşil.

Örnek 9.9

Aynı paintComponent 240 × 180 piksellik bir panelde çalışırsa değişkenler ve üç çizim çağrısının argümanları ne olur?

Merkez (120, 90)'dır. r = min(120, 90) − 5 = 85. r2 = (int) (85 × 0,9) = (int) 76,5 = 76. bw = (int) (76 × 1,4) = (int) 106,4 = 106; bh = (int) (76 × 0,35) = (int) 26,6 = 26. Kesme 26,6'yı bile 26'ya indirir. Çağrılar şunlardır:

setColor(0, 178, 0)
fillRect(0, 0, 240, 180)
setColor(255, 255, 255)
fillOval(35, 5, 170, 170)
setColor(255, 0, 0)
fillOval(44, 14, 152, 152)
setColor(255, 255, 255)
fillRect(67, 77, 106, 26)

Dış daire (120 − 85, 90 − 85, 170, 170), iç daire (120 − 76, 90 − 76, 152, 152)'dir; halka 9 piksel kalınlığındadır. Bar için bw / 2 = 53, bh / 2 = 13: (67, 77, 106, 26).

Örnek 9.10

Panel 200 × 100 piksel olsaydı r, r2, bw ve bh kaç olurdu?

r = min(100, 50) − 5 = 45. r2 = (int) 40,5 = 40. bw = (int) (40 × 1,4) ve bh = (int) (40 × 0,35)'tir. Burada bir kayan nokta tuzağı akla gelebilir: 1.4 ve 0.35 ikilik sistemde tam gösterilemez, çarpım 55,999… çıkıp 55'e kesilebilir mi? Java'da denendiğinde 40 * 1.4 tam 56.0, 40 * 0.35 tam 14.0 verdi. Sonuç bw = 56, bh = 14'tür. Bu tür sınır durumlarında sonuca güvenmenin yolu hesabı çalıştırmaktır.

Sık yapılan hata

Katmanların sırasını karıştırmak. Kırmızı daire beyazdan önce çizilirse büyük beyaz daire onu tümüyle örter ve işaret beyaz bir daireye döner. Bar kırmızıdan önce çizilirse kırmızı daire barı örter. Sonradan çizilen her zaman üsttedir; çizim sırası katman sırasıdır.

9.5.4main ve pencere

public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> {
        var panel = new GirisYasaktirIsareti();
        panel.setBackground(Color.GREEN.darker());
        var frame = new JFrame("Basit bir grafik programı");
        frame.setSize(400, 300);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(panel, BorderLayout.CENTER);
        frame.setVisible(true);
    });
}

main metodunun tek işi, arayüzü kuracak kodu SwingUtilities.invokeLater'a vermektir. () -> { … } bir lambda ifadesidir: “sonra çalıştırılacak kod parçası”. invokeLater bu parçayı olay iş parçacığında çalıştırılmak üzere kuyruğa koyar. Bu kalıp her Swing programının main'inde bulunur; gerekçesi olay iş parçacığının çalışma kuralıdır.

Lambdanın içinde panel oluşturulur ve arka planı Color.GREEN.darker(), yani (0, 178, 0) yapılır. Saf yeşil (0, 255, 0) göz yoracak kadar parlaktır; koyu ton trafik işaretine daha çok benzer. super.paintComponent(g) zemini bu renkle boyar. Panel, pencerenin içerik bölmesinin merkezine eklenir. Tek bileşen olduğu için bütün alanı kaplar.

400 × 300 pencerede kenarlık ve başlık çubuğu düşülünce panel yaklaşık 384 × 261 olur. Bu değer işletim sistemine göre değişir. Programın doğru çalışması bu değere bağlı değildir: ölçüler her çizimde getWidth() ve getHeight()'tan yeniden hesaplanır. Panel 768 × 522'ye büyütülürse dış daire fillOval(128, 5, 512, 512) ile çizilir ve işaret yine ortadadır.

9.6Fare olayları ve olay iş parçacığı

9.6.1Etkileşimli grafik ve olay döngüsü

Bilgisayar grafikleri, görsel içerik oluşturma ve işleme sürecidir. Etkileşimli (interactive) grafiklerde kullanıcı fare, klavye gibi giriş aygıtlarıyla görüntüyü değiştirir. Çizim programları, oyunlar ve haritalar bu türdendir. Grafik kütüphaneleri şekil çizmeyi ve renklendirmeyi sağlar; etkileşim ise bir olay döngüsüyle kurulur.

Kullanıcıfare, klavyeİşletim sistemigiriş aygıtı sinyaliOlay kuyruğuMouseEvent, KeyEvent…Olay iş parçacığı(EDT) sıradaki olayı alırDinleyici metotmousePressed(e)…repaint()yeniden çizim isteğipaintComponent(g)Swing çağırırEkrangüncel görüntüsıradaki olay
Şekil 9.18: Olay döngüsü. Kullanıcının eylemi işletim sisteminden geçip olay kuyruğuna girer. Olay iş parçacığı olayı dinleyici metoda verir; metot veriyi günceller ve repaint() ister. Swing uygun anda paintComponent'i çağırır ve ekran güncellenir.

Döngüdeki adımlar sırasıyla şunlardır. Kullanıcı fareye basar. İşletim sistemi bu eylemi algılar ve Java'ya bildirir. Java bir olay nesnesi oluşturur ve olay kuyruğuna koyar. Olay iş parçacığı kuyruktaki sıradaki olayı alır ve o bileşene kayıtlı dinleyicinin metodunu çağırır. Dinleyici metot programın verisini günceller ve repaint() ile yeniden çizim ister. Swing uygun bir anda paintComponent'i çağırır; ekran yeni veriyle çizilir. Olay iş parçacığı sıradaki olaya geçer.

Bu döngünün bir kuralı vardır: dinleyici metot ekranı doğrudan boyamaz. Yalnız veriyi değiştirir ve yeniden çizim ister. Çizimi her zaman paintComponent yapar. Bu ayrım, çizimin tek bir yerde toplanmasını ve pencere her yeniden boyandığında doğru görüntünün oluşmasını sağlar.

9.6.2Dinleyici arayüzü ve adaptör sınıfı

Tanım 9.11

Olay dinleyici (event listener), bir olay olunca çağrılacak metotları tanımlayan arayüz ya da bu arayüzü uygulayan nesnedir. Dinleyici nesne bir bileşene add…Listener metoduyla kaydedilir.

Fare olayları için Java'nın arayüzü MouseListener'dır. Arayüzün beş soyut metodu vardır. Bir sınıf bu arayüzü uygularsa beş metodun hepsini yazmak zorundadır. İlgilenmediği metotların gövdesi boş kalır.

class Dinleyici implements MouseListener {
    public void mouseClicked(MouseEvent e) {
        System.out.println("tık");
    }
    public void mousePressed(MouseEvent e) { }
    public void mouseReleased(MouseEvent e) { }
    public void mouseEntered(MouseEvent e) { }
    public void mouseExited(MouseEvent e) { }
}

Beş metottan dördü boştur. Yalnız tıklamayla ilgilenen bir sınıf için bu fazla yazımdır. Java bu durum için hazır sınıflar sunar.

Tanım 9.12

Adaptör sınıfı (adapter class), bir dinleyici arayüzünün bütün metotlarını boş gövdelerle uygulayan hazır sınıftır. Ondan türeyen sınıf yalnız ilgilendiği metodu geçersiz kılar.

class Dinleyici extends MouseAdapter {
    @Override
    public void mouseClicked(MouseEvent e) {
        System.out.println("tık");
    }
}
// panel.addMouseListener(new Dinleyici());

MouseAdapter soyut bir sınıftır, ama soyut metodu yoktur; bütün metotları boş gövdelidir. Soyut tanımlanmasının amacı, doğrudan nesnesinin oluşturulmasını engellemektir. MouseAdapter üç arayüzü birden uygular: MouseListener, MouseMotionListener ve MouseWheelListener. Tek bir alt sınıf böylece tıklama, hareket ve tekerlek olaylarının hepsini yazabilir.

Hareket olayları ayrı bir arayüzdedir: MouseMotionListener. Bu arayüzün iki metodu vardır: mouseDragged ve mouseMoved. Kendi adaptörü MouseMotionAdapter'dır. Kayıt da ayrıdır: tıklama türü olaylar addMouseListener(…), hareket olayları addMouseMotionListener(…) ile kaydedilir.

Dinleyici sınıfı ayrı bir dosyada yazmak da gerekmez. Java, tek kullanımlık sınıfı kullanıldığı yerde tanımlamaya izin verir.

Tanım 9.13

Anonim iç sınıf (anonymous inner class), adı olmayan, tanımlandığı yerde tek nesnesi oluşturulan iç sınıftır. new MouseAdapter() { … } yazımı, MouseAdapter'dan türeyen adsız bir sınıf tanımlar ve hemen bir nesnesini oluşturur.

Sık yapılan hata

Bir arayüzü uygulayıp metotlarının bir kısmını yazmamak. class Dinleyici implements MouseListener yalnız mouseClicked'ı yazarsa derleyici şu hatayı verir:

Dinleyici.java:6: error: Dinleyici is not abstract and does not override abstract method mouseExited(MouseEvent) in MouseListener
class Dinleyici implements MouseListener {

Derleyici eksik metotlardan yalnız birini adıyla söyler. mousePressed de yazılsa ileti yine mouseExited'ı gösterir; ileti eksik metotların listesi değildir. Çözüm ya beş metodu da yazmak ya da MouseAdapter'dan türetmektir.

9.6.3Fare olay metotları

Tablo 9.11: Fare olay metotları.
MetotArayüzNe zaman çağrılır?
mousePressed(e)MouseListenerFare düğmesine basıldığı an
mouseReleased(e)MouseListenerDüğme bırakıldığında
mouseClicked(e)MouseListenerBasıp aynı yerde bırakınca (pressed → released → clicked)
mouseEntered(e) / mouseExited(e)MouseListenerİşaretçi bileşenin içine girince / dışına çıkınca
mouseMoved(e)MouseMotionListenerDüğmeye basmadan hareket edince
mouseDragged(e)MouseMotionListenerDüğme basılıyken hareket edince (sürükleme)
mouseWheelMoved(e)MouseWheelListenerTekerlek döndürülünce
tıklama:mouseEnteredmousePressedmouseReleasedmouseClickedmouseExitedsürüklemede: mousePressed → mouseDragged … → mouseReleased; işaretçi yer değiştirdiği için mouseClicked gelmez
Şekil 9.19: Fare olaylarının sırası. Tıklama üç olay üretir; sürükleme, basma ile bırakma arasında çok sayıda mouseDragged üretir.

Bir tıklama tek bir olay değildir. Kullanıcı düğmeye basar: mousePressed. Bırakır: mouseReleased. Basma ve bırakma aynı noktada olduysa Java bir de mouseClicked üretir. Basma ile bırakma arasında fare hareket ettiyse mouseClicked gelmez; araya mouseDragged olayları girer. Bu yüzden çizim programı gibi uygulamalar tıklamayı değil, basmayı ve sürüklemeyi dinler.

Başsız kipte bir panele sırayla basma, bırakma ve tıklama olayları gönderildiğinde dinleyiciler bu sırayla çağrıldı. Aynı panele hem Dinleyici (arayüzü uygulayan) hem Dinleyici2 (adaptörden türeyen) kaydedildiği için tıklamada iki kez “tık” yazıldı. Bir bileşene birden çok dinleyici kaydedilebilir; olay geldiğinde hepsi çağrılır.

9.6.4MouseEvent sınıfı

Her fare metodu bir MouseEvent e parametresi alır. Olayın nerede, hangi düğmeyle ve hangi tuşlar basılıyken olduğu bu nesnededir.

Tablo 9.12: MouseEvent sınıfının sık kullanılan metotları.
MetotDöndürdüğü
getX(), getY()İşaretçinin bileşene göre konumu
getPoint()Aynı konum, Point olarak
getButton()BUTTON1 sol, BUTTON2 orta, BUTTON3 sağ (değerleri 1, 2, 3)
getClickCount()Art arda tıklama sayısı (2: çift tık)
isShiftDown(), isCtrlDown(), isAltDown()O anda tuş basılı mı?
panel.addMouseListener(new MouseAdapter() {
    @Override
    public void mousePressed(MouseEvent e) {
        String dugme = "sol";
        if (e.getButton() == MouseEvent.BUTTON3) {
            dugme = "sağ";
        }
        System.out.println(dugme + " (" + e.getX()
                + ", " + e.getY() + ")"
                + (e.isShiftDown() ? " + Shift" : ""));
    }
});

Kod bir panele anonim bir adaptör kaydeder. Basılan düğme sağ düğmeyse dugme değişkeni “sağ” olur. Ardından düğme adı, konum ve Shift tuşu basılıysa “ + Shift” yazılır. Panele (40, 120)'de sol tıklandığında sol (40, 120), Shift basılıyken (70, 30)'da sağ tıklandığında sağ (70, 30) + Shift yazdığı çalıştırılarak görüldü.

Koordinatlar ekrana değil, olayı alan bileşene göredir. Panelin sol üst köşesi, panel pencerenin neresinde olursa olsun, (0, 0)'dır. Bu sayede panelin içinde çizilen şekillerle fare koordinatları doğrudan karşılaştırılabilir.

Özetle, bir tıklama pressed → released → clicked sırasıyla üç olay üretir; sürüklerken ise çok sayıda mouseDragged gelir. Sağ tık BUTTON3'tür.

9.6.5Olay iş parçacığı (EDT)

Swing programlarının main'i SwingUtilities.invokeLater kalıbıyla yazılır. Bu kalıbın nedeni, Swing'in iş parçacıklarıyla ilişkisidir.

Tanım 9.14

Olay iş parçacığı (Event Dispatch Thread, EDT), Swing bileşenlerinin kurulduğu, değiştirildiği ve olayların işlendiği tek iş parçacığıdır. Olay kuyruğundaki işleri sırayla, birer birer çalıştırır. Java 21'de adı AWT-EventQueue-0'dır.

Swing bileşenleri iş parçacığı güvenli değildir (not thread-safe). Aynı bileşene aynı anda birden çok iş parçacığı dokunursa bileşenin iç durumu tutarsız hâle gelebilir. Swing bu sorunu kilitlerle değil, basit bir kuralla çözer: bileşenler yalnız olay iş parçacığından kurulur ve değiştirilir. Dinleyici metotlar zaten EDT'de çalışır; onların içinden bileşenlere dokunmak güvenlidir.

Bir benzetme kuralı açıklar. Mutfakta tek bir aşçı vardır: EDT. Garsonlar yemeği kendileri yapmaz; sipariş fişi bırakır. Fiş bir Runnable nesnesidir, yani çalıştırılacak bir kod parçası. Aşçı fişleri geliş sırasıyla, birer birer hazırlar. Tek aşçı olduğu için iki yemek aynı tavaya aynı anda girmez.

main iş parçacığımain biterinvokeLater(iş)olay kuyruğuişfaretuşboyaolay iş parçacığı (EDT)sırayla tek tek çalıştırırpencere kapanana kadar sürer
Şekil 9.20: main iş parçacığı işi invokeLater ile olay kuyruğuna koyar ve biter. Olay iş parçacığı kuyruktaki işleri sırayla çalıştırır.
Tablo 9.13: SwingUtilities'in iki çağırma metodu.
MetotDavranış
invokeLater(iş)İşi kuyruğa koyar ve hemen döner (asenkron). GUI başlatmanın standart yolu.
invokeAndWait(iş)İş EDT'de bitene kadar bekler (senkron). EDT'nin içinden çağrılırsa Error fırlatır: EDT kendini bekleyemez.

invokeAndWait'in EDT içinden çağrılması gerçekten bir Error fırlatır. EDT, kuyruğa koyduğu işin bitmesini beklerse o işi çalıştıracak kimse kalmaz; program sonsuza kadar kilitlenirdi. Swing bunu önceden yakalar:

AWT-EventQueue-0 daemon=false
java.lang.Error: Cannot call invokeAndWait from the event dispatcher thread

İlk satır, invokeAndWait ile çalıştırılan işin hangi iş parçacığında olduğunu gösterir: AWT-EventQueue-0. Bu iş parçacığı arka plan (daemon) iş parçacığı değildir. JVM, arka plan olmayan bir iş parçacığı çalıştıkça kapanmaz. GUI programının main bittikten sonra da sürmesinin nedeni budur.

9.6.6Kod izleme: invokeLater ve invokeAndWait

public static void main(String[] a) throws Exception {
  System.out.println("1 main");
  SwingUtilities.invokeLater(
      () -> System.out.println("3 iş A"));
  SwingUtilities.invokeLater(
      () -> System.out.println("4 iş B"));
  System.out.println("2 main devam ediyor");
  SwingUtilities.invokeAndWait(() -> System.out.println(
      "5 iş C, EDT mi? " + SwingUtilities.isEventDispatchThread()));
  System.out.println("6 main, EDT mi? "
      + SwingUtilities.isEventDispatchThread());
}
1 main
2 main devam ediyor
3 iş A
4 iş B
5 iş C, EDT mi? true
6 main, EDT mi? false
Tablo 9.14: Olay kuyruğu izlemesi.
AdımSatırDeğişkenlerYeni çıktı
12: System.out.println("1 main");iş parçacığı=main1 main
23: SwingUtilities.invokeLater(
4: () -> System.out.println("3 iş A"));
kuyruk=[A]—
35: SwingUtilities.invokeLater(
6: () -> System.out.println("4 iş B"));
kuyruk=[A, B]—
47: System.out.println("2 main devam ediyor");kuyruk=[A, B]2 main devam ediyor
58: SwingUtilities.invokeAndWait(() -> System.out.println(
9: "5 iş C, EDT mi? " + SwingUtilities.isEventDispatchThread()));
kuyruk=[A, B, C]—
64: () -> System.out.println("3 iş A"));kuyruk=[B, C]3 iş A
76: () -> System.out.println("4 iş B"));kuyruk=[C]4 iş B
88: SwingUtilities.invokeAndWait(() -> System.out.println(
9: "5 iş C, EDT mi? " + SwingUtilities.isEventDispatchThread()));
kuyruk=[]5 iş C, EDT mi? true
910: System.out.println("6 main, EDT mi? "
11: + SwingUtilities.isEventDispatchThread());
iş parçacığı=main6 main, EDT mi? false

Program main iş parçacığında başlar ve 1 main yazar. 2. adımda invokeLater iş A'yı kuyruğa koyar ve hemen döner. Olay iş parçacığı henüz yoksa bu ilk çağrı onu başlatır. 3. adımda iş B de kuyruğa eklenir; main yine beklemez. 4. adımda main kendi işine devam eder ve 2 main devam ediyor yazar.

mainolay kuyruğuEDTbekliyoriş C bitene dekABCilk giren ilk çıkarboş
Şekil 9.21: 5. adım. invokeAndWait iş C'yi kuyruğa koydu ve main'i durdurdu. Kuyrukta A, B ve C bekliyor.

5. adımda invokeAndWait iş C'yi kuyruğa koyar ve main'i durdurur. EDT kuyruğu baştan işler: önce A (3 iş A), sonra B (4 iş B), sonra C. Kuyruk sıralıdır; işler kuyruğa giriş sırasıyla çalışır. İş C, isEventDispatchThread() ile kendi iş parçacığını sorar ve true alır. C bitince bekleyen main uyandırılır. Son satırda main aynı soruyu sorar ve false alır.

Çıktının sırası hakkında dikkatli olmak gerekir. 1, 2, 3, 4, 5, 6 sırası bu çalıştırmada görüldü. Program 20 kez çalıştırıldığında da sıra her seferinde aynı çıktı. Ama sıranın bir kısmı garanti değildir. 2 main devam ediyor satırı ile 3 iş A satırı farklı iş parçacıklarından gelir. EDT, main 2. satırı yazmadan önce A'yı çalıştırabilirdi. Kesin olan şunlardır: A, B'den önce; B, C'den önce; C, 6'dan önce çalışır. Aynı ayrım invokeLater(B), println(A), invokeAndWait(C), println(D) dizisinde de geçerlidir. B, C'den ve C, D'den önce gelir. 20 denemede hep ABCD görüldü, ama A ile B'nin yeri çalıştırmaya göre değişebilir.

Örnek 9.11

Bir düğme dinleyicisi sırasıyla System.out.println("1"), SwingUtilities.invokeLater(() -> System.out.println("3")) ve System.out.println("2") çalıştırıyor. Çıktı nedir? Sıra kesin midir?

1, 2, 3. Dinleyici EDT'de çalışır. invokeLater işi kuyruğun sonuna koyar ve döner; dinleyici 2'yi yazar ve biter. EDT ancak bundan sonra kuyruktaki sıradaki işi alabilir. Bu kez sıra kesindir, çünkü iki parça da aynı iş parçacığında çalışır. Kuyruğa konan iş, o anda çalışan işten önce başlayamaz.

Sık yapılan hata

Arayüzü doğrudan main iş parçacığında kurmak. Küçük programlarda çoğu zaman sorun çıkmaz; bu yüzden hata uzun süre fark edilmez. Ama pencere görünür olduktan sonra EDT çalışmaya başlar ve main ile aynı bileşenlere dokunabilir. Sonuç, nadiren ve rastgele görülen çizim bozuklukları ya da istisnalardır. Güvenli kalıp tektir: SwingUtilities.invokeLater(() -> { … }).

9.7Uygulama: çizim programı

Bu uygulama, fareyle basılı tutup sürükleyerek çizim yapılan bir programdır. Çalışma ilkesi üç cümleyle özetlenir:

  • Basınca (mousePressed) yeni bir eğri başlar.
  • Sürükledikçe (mouseDragged) eğrinin sonuna nokta eklenir ve yeniden çizim istenir.
  • Çizerken (paintComponent) her eğrinin ardışık noktaları çizgilerle birleştirilir.
Şekil 9.22: Fareyle çizim yapılan panel.

Programın temel tasarım kararı, çizimi bir resim olarak değil, noktaların listesi olarak saklamaktır. Ekran her seferinde bu veriden yeniden çizilir. Pencere küçültülüp büyütülse de, üstüne başka bir pencere gelip çekilse de çizim kaybolmaz. Veri ile görüntünün ayrılması, olay döngüsünün kuralını uygular: dinleyiciler veriyi değiştirir, paintComponent veriyi çizer.

9.7.1Eğrileri saklamak

public class BasitCizimPaneli extends JPanel {
    private List<List<Point>> egriler = new ArrayList<>();

    public BasitCizimPaneli() {
        addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                var yeniEgri = new ArrayList<Point>();
                yeniEgri.add(new Point(e.getX(), e.getY()));
                egriler.add(yeniEgri);
            }
        });
        addMouseMotionListener(new MouseMotionAdapter() {
            @Override
            public void mouseDragged(MouseEvent e) {
                egriler.get(egriler.size() - 1)
                       .add(new Point(e.getX(), e.getY()));
                repaint(0, 0, getWidth(), getHeight());
            }
        });
    }
}

Sınıfın tek alanı egriler'dir. Tipi List<List<Point>>, yani listelerin listesidir. Dış liste eğrileri tutar. Her iç liste bir eğrinin noktalarını sırayla tutar.

eğri 0:(40,120)(60,60)(80,120)eğri 1:(50,95)(70,95)
Şekil 9.23: İki eğrili bir çizim ve onu tutan veri. Eğri 0 üç, eğri 1 iki noktadan oluşur.

Yapıcı iki dinleyici kaydeder. Birincisi MouseAdapter'dan türeyen anonim bir sınıftır ve yalnız mousePressed'ı yazar. Metot yeni ve boş bir nokta listesi oluşturur, basılan noktayı listeye ekler ve listeyi dış listeye koyar. İkinci dinleyici MouseMotionAdapter'dan türer ve yalnız mouseDragged'ı yazar. Metot dış listenin son öğesini, yani en son başlayan eğriyi alır ve sonuna yeni noktayı ekler. Ardından paneli yeniden çizdirir.

Son eğriye egriler.get(egriler.size() - 1) ile ulaşılır. Liste indisleri 0'dan başladığı için son öğenin indisi size() - 1'dir. get(size()) her zaman listenin bir ötesini ister.

Sık yapılan hata

Son öğeyi get(size()) ile istemek. Listede bir eğri varken bu çağrı şu istisnayı fırlatır:

java.lang.IndexOutOfBoundsException: Index 1 out of bounds for length 1

Hata derleme anında yakalanmaz; ilk sürüklemede ortaya çıkar. Doğru indis size() - 1'dir.

Kodun bir de içe aktarma tuzağı vardır. java.awt.* ve java.util.* birlikte içe aktarılırsa List adı iki pakette birden bulunur: java.awt.List bir liste kutusu bileşeni, java.util.List ise koleksiyon arayüzüdür. Derleyici hangisinin kastedildiğini bilemez:

Ana.java:4: error: reference to List is ambiguous
  List<String> x;
  both interface java.util.List in java.util and class java.awt.List in java.awt match

Çözüm, kastedilen sınıfı ayrıca açıkça içe aktarmaktır: import java.util.List;. Tek tip içe aktarma, yıldızlı içe aktarmadan önceliklidir.

9.7.2Kod izleme: fare olayları

Kullanıcı şu hareketleri yapsın: (40, 120)'de basar, (60, 60) ve (80, 120)'ye sürükler, bırakır; sonra (50, 95)'te yeniden basar ve (70, 95)'e sürükler.

addMouseListener(new MouseAdapter() {
    @Override
    public void mousePressed(MouseEvent e) {
        var yeniEgri = new ArrayList<Point>();
        yeniEgri.add(new Point(e.getX(), e.getY()));
        egriler.add(yeniEgri);
    }
});
addMouseMotionListener(new MouseMotionAdapter() {
    @Override
    public void mouseDragged(MouseEvent e) {
        egriler.get(egriler.size() - 1)
               .add(new Point(e.getX(), e.getY()));
        repaint(0, 0, getWidth(), getHeight());
    }
});
Tablo 9.15: Fare olaylarının izlemesi.
AdımSatırDeğişkenlerAçıklama
14: var yeniEgri = new ArrayList<Point>();
5: yeniEgri.add(new Point(e.getX(), e.getY()));
6: egriler.add(yeniEgri);
e.getX()=40, e.getY()=120, egriler.size()=1mousePressed: Basıldı (40, 120): yeni bir iç liste oluşur, ilk noktası eklenir, dış listeye konur.
212: egriler.get(egriler.size() - 1)
13: .add(new Point(e.getX(), e.getY()));
14: repaint(0, 0, getWidth(), getHeight());
e.getX()=60, e.getY()=60, egriler.size()=1mouseDragged: Sürüklendi (60, 60): son eğriye nokta eklenir; repaint yeniden çizimi ister, çizgi görünür.
312: egriler.get(egriler.size() - 1)
13: .add(new Point(e.getX(), e.getY()));
14: repaint(0, 0, getWidth(), getHeight());
e.getX()=80, e.getY()=120, egriler.size()=1mouseDragged: Sürüklendi (80, 120): aynı eğri uzar. Gerçekte saniyede onlarca mouseDragged gelir.
41: addMouseListener(new MouseAdapter() {e.getX()=80, e.getY()=120, egriler.size()=1mouseReleased: Bırakıldı: mouseReleased'ı yazmadık; MouseAdapter'ın boş gövdesi çalışır, hiçbir şey olmaz.
54: var yeniEgri = new ArrayList<Point>();
5: yeniEgri.add(new Point(e.getX(), e.getY()));
6: egriler.add(yeniEgri);
e.getX()=50, e.getY()=95, egriler.size()=2mousePressed: Yeniden basıldı (50, 95): ikinci eğri başlar. Eski eğri olduğu gibi kalır.
612: egriler.get(egriler.size() - 1)
13: .add(new Point(e.getX(), e.getY()));
14: repaint(0, 0, getWidth(), getHeight());
e.getX()=70, e.getY()=95, egriler.size()=2mouseDragged: Sürüklendi (70, 95): ekleme yine son eğriye, yani eğri 1'e yapılır.

1. adımda dış liste boştur. mousePressed yeni bir iç liste oluşturur, (40, 120)'yi ekler ve iç listeyi dış listeye koyar: egriler.size() 1 olur. 2. ve 3. adımlarda mouseDragged iki nokta ekler. İkisi de son eğriye, yani eğri 0'a gider. Her eklemeden sonra repaint yeniden çizim ister ve yeni çizgi parçası ekranda görünür.

Gerçek kullanımda olay sayısı çok daha fazladır. Fare hareket ettikçe işletim sistemi saniyede onlarca konum bildirir ve her biri bir mouseDragged olur. İzlemedeki iki sürükleme, bu akışın iki örneğidir.

4. adımda kullanıcı fareyi bırakır. mouseReleased'ı yazmadık. Olay yine gelir, ama MouseAdapter'ın boş gövdesi çalışır ve hiçbir şey olmaz. 5. adımda yeni bir basma ikinci bir iç liste başlatır; eski eğri olduğu gibi kalır. 6. adımdaki sürükleme yine son eğriye, artık eğri 1'e eklenir.

Olay dizisi başsız kipte panele doğrudan gönderilerek denendi. Her adımdan sonra yazılan veri, tablonun son durumunu verdi: [[(40,120), (60,60), (80,120)], [(50,95), (70,95)]].

9.7.3Kod izleme: eğrileri çizmek

@Override
public void paintComponent(Graphics g) {
    super.paintComponent(g);
    for (var egri : egriler) {
        var onceki = egri.get(0);
        for (var nokta : egri) {
            g.drawLine(onceki.x, onceki.y, nokta.x, nokta.y);
            onceki = nokta;
        }
    }
}

Metot iç içe iki döngü kullanır. Dış döngü eğrileri, iç döngü bir eğrinin noktalarını dolaşır. onceki değişkeni, çizginin başlayacağı noktayı tutar. Her eğrinin başında onceki o eğrinin ilk noktasına ayarlanır. İç döngü her noktada onceki'den o noktaya bir çizgi çizer ve onceki'yi ilerletir.

Tablo 9.16: paintComponent izlemesi; “Yeni çıktı” sütunu yapılan çizim çağrısıdır.
AdımSatırDeğişkenlerYeni çıktı
13: super.paintComponent(g);——
24: for (var egri : egriler) {
5: var onceki = egri.get(0);
egri=eğri 0, onceki=(40,120)—
36: for (var nokta : egri) {
7: g.drawLine(onceki.x, onceki.y, nokta.x, nokta.y);
8: onceki = nokta;
egri=eğri 0, onceki=(40,120), nokta=(40,120)drawLine(40, 120, 40, 120)
46: for (var nokta : egri) {
7: g.drawLine(onceki.x, onceki.y, nokta.x, nokta.y);
8: onceki = nokta;
egri=eğri 0, onceki=(40,120), nokta=(60,60)drawLine(40, 120, 60, 60)
56: for (var nokta : egri) {
7: g.drawLine(onceki.x, onceki.y, nokta.x, nokta.y);
8: onceki = nokta;
egri=eğri 0, onceki=(60,60), nokta=(80,120)drawLine(60, 60, 80, 120)
64: for (var egri : egriler) {
5: var onceki = egri.get(0);
egri=eğri 1, onceki=(50,95)—
76: for (var nokta : egri) {
7: g.drawLine(onceki.x, onceki.y, nokta.x, nokta.y);
8: onceki = nokta;
egri=eğri 1, onceki=(50,95), nokta=(50,95)drawLine(50, 95, 50, 95)
86: for (var nokta : egri) {
7: g.drawLine(onceki.x, onceki.y, nokta.x, nokta.y);
8: onceki = nokta;
egri=eğri 1, onceki=(50,95), nokta=(70,95)drawLine(50, 95, 70, 95)

3. adımda bir ayrıntı göze çarpar. İç döngü ilk noktadan başlar ve onceki de ilk noktadır. İlk çağrı drawLine(40, 120, 40, 120) olur: nokta kendisiyle birleşir ve tek bir piksel çizilir. Ardından onceki = nokta yapılır. 4. ve 5. adımlar gerçek çizgileri çizer: (40,120) → (60,60) ve (60,60) → (80,120).

Şekil 9.24: 8. adım. Beş drawLine çağrısıyla çizim tamamlandı; iki eğri birbirine bağlanmadı.

6. adımda dış döngü ikinci eğriye geçer ve onceki yeniden bu eğrinin ilk noktası olur. İki eğrinin birleşmemesi bu satırın işidir. onceki her eğride sıfırlanmasaydı, eğri 0'ın son noktası (80, 120) ile eğri 1'in ilk noktası (50, 95) arasında istenmeyen bir çizgi çizilirdi. Toplam çağrı sayısı 3 + 2 = 5'tir; görünen çizgi parçası ise 2 + 1 = 3'tür.

DebugGraphics kaydı da aynı beş çağrıyı verdi:

setColor(238, 238, 238)
fillRect(0, 0, 200, 200)
drawLine(40, 120, 40, 120)
drawLine(40, 120, 60, 60)
drawLine(60, 60, 80, 120)
drawLine(50, 95, 50, 95)
drawLine(50, 95, 70, 95)

İlk iki satırı yine super.paintComponent(g) üretti: zemin, panelin arka plan rengiyle temizlendi. Önceki çizimden iz kalmaz. Yeni görüntü her seferinde boş bir zeminden, verinin tamamından kurulur.

Örnek 9.12

Kullanıcı (10, 10)'da basar, (20, 10) ve (30, 20)'ye sürükler, bırakır; (5, 5)'te basar, (6, 6)'ya sürükler, bırakır. egriler.size(), her eğrinin nokta sayısı ve yeniden çizimdeki drawLine sayısı nedir?

Her basış bir iç liste eklediği için egriler.size() 2'dir. Eğri 0'da üç nokta, eğri 1'de iki nokta vardır. drawLine sayısı nokta sayısına eşittir: 3 + 2 = 5. Kayıt şöyledir:

setColor(238, 238, 238)
fillRect(0, 0, 200, 200)
drawLine(10, 10, 10, 10)
drawLine(10, 10, 20, 10)
drawLine(20, 10, 30, 20)
drawLine(5, 5, 5, 5)
drawLine(5, 5, 6, 6)

9.7.4main ve repaint

public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> {
        var frame = new JFrame("Basit Çizim Programı");
        frame.getContentPane().add(new BasitCizimPaneli(),
                BorderLayout.CENTER);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(400, 300);
        frame.setVisible(true);
    });
}

main bilinen kalıbı izler: arayüz invokeLater ile EDT'de kurulur, panel içerik bölmesinin merkezine eklenir, pencere 400 × 300 boyutunda gösterilir.

repaint metodu iki biçimde kullanılır ve ikisi de bir istektir:

  • repaint() çizimi hemen yapmaz; kuyruğa bir “yeniden boya” isteği koyar. Art arda gelen istekler tek bir boyamada birleştirilebilir. Fare saniyede onlarca olay üretirken bu birleştirme gereksiz çizimleri önler.
  • repaint(x, y, en, boy) yalnız o dikdörtgenin yeniden çizilmesini ister. Swing bu bölgenin dışını kırpar (clipping): g bölgenin dışına boyamaz ve iş azalır. repaint(0, 0, getWidth(), getHeight()), repaint() ile aynıdır.
Tanım 9.15

repaint(), bir bileşenin yeniden çizilmesi için olay kuyruğuna istek bırakan metottur. Swing uygun anda bileşenin paintComponent metodunu çağırır. Programcı paintComponent'i kendisi çağırmaz; veriyi değiştirir ve repaint() ister.

Sık yapılan hata

Dinleyicinin içinden paintComponent(getGraphics()) çağırmak. Bu çağrı Swing'in boyama düzenini atlar. Çift arabellekleme, yani görüntünün önce bellekte hazırlanması devre dışı kalır ve görüntü titreyebilir. Bir sonraki gerçek boyamada da çizilen her şey silinir. Doğru yol repaint()'tir.

9.8Düğme ve metin bileşenleri

9.8.1JButton sınıfı

JButton, tıklanabilir düğmeler oluşturur. Metni setText(), ikonu setIcon() ile belirlenir. setEnabled(false) düğmeyi pasif yapar: düğme soluk görünür ve tıklanınca olay üretmez.

Düğmenin tıklanmasına yanıt vermek için addActionListener() ile bir dinleyici eklenir. Dinleyici ActionListener arayüzünü uygular. Bu arayüzün tek metodu vardır: actionPerformed(ActionEvent e). Düğme, klavyeden boşluk tuşuyla da tetiklenebilir; iki durumda da aynı metot çağrılır. Bu yüzden düğme için fare olayı değil, “eylem” olayı dinlenir.

Dinleyici eklemek, Gözlemci (Observer) tasarım deseninin bir örneğidir. Düğme “konu”dur; kendisine kayıtlı gözlemcilerin listesini tutar. Bir tıklama olunca listedeki her gözlemciye haber verir. Düğme, gözlemcilerin ne yaptığını bilmez. Yeni bir davranış eklemek için düğmenin koduna dokunmak gerekmez; yalnız yeni bir gözlemci kaydedilir.

class SayacPaneli extends JPanel {
    private int sayac = 0;
    private JLabel etiket = new JLabel("Henüz tıklanmadı");

    SayacPaneli() {
        JButton dugme = new JButton("Tıkla");
        dugme.addActionListener(e -> {
            sayac++;
            etiket.setText(sayac + " kez tıklandı");
        });
        add(dugme);
        add(etiket);
    }
}

9.8.2Kod izleme: sayaç düğmesi

Tablo 9.17: Sayaç düğmesinin izlemesi.
AdımSatırÇağrı yığınıDeğişkenlerAçıklama
12: private int sayac = 0;
3: private JLabel etiket = new JLabel("Henüz tıklanmadı");
main() → new SayacPaneli()sayac=0, etiket=JLabelNesne oluşurken önce alanlara ilk değerleri verilir: sayac 0, etiket hazır.
26: JButton dugme = new JButton("Tıkla");main() → SayacPaneli()dugme=JButton("Tıkla")Yapıcı bir düğme oluşturur; dugme yerel değişkendir.
37: dugme.addActionListener(e -> {
8: sayac++;
9: etiket.setText(sayac + " kez tıklandı");
10: });
main() → SayacPaneli()dinleyici sayısı=1Lambda çalıştırılmaz, yalnızca düğmeye kaydedilir. Gövdesi tıklanınca çalışacak.
411: add(dugme);
12: add(etiket);
main()bileşen sayısı=2Düğme ve etiket panele eklenir (FlowLayout: yan yana). Yapıcı biter.
58: sayac++;EDT → actionPerformed(e)sayac=1Kullanıcı tıklar: EDT actionPerformed'ı, yani lambda gövdesini çağırır. sayac bir alan olduğu için artırılabilir.
69: etiket.setText(sayac + " kez tıklandı");EDT → actionPerformed(e)sayac=1setText etiketin metnini değiştirir; Swing etiketi yeniden boyar.
78: sayac++;EDT → actionPerformed(e)sayac=2İkinci tıklama: aynı lambda yeniden çağrılır, alan 2 olur.
89: etiket.setText(sayac + " kez tıklandı");EDT → actionPerformed(e)sayac=2Etiket güncellenir. Tıklamalar arasında program hiçbir kod çalıştırmaz; yalnızca olay bekler.

1. adımda new SayacPaneli() çalışmaya başlar. Yapıcının gövdesinden önce alanlara ilk değerleri verilir: sayac 0 olur, etiket “Henüz tıklanmadı” metniyle oluşturulur. 2. adımda yapıcı bir düğme oluşturur. dugme yerel bir değişkendir; yapıcı bitince değişken kaybolur, ama düğme panelin bileşeni olarak yaşamaya devam eder.

3. adım izlemenin en önemli adımıdır. Lambda ifadesi bu anda çalıştırılmaz. Yalnız bir nesne olarak düğmeye kaydedilir. Gövdesindeki sayac++ ve setText ancak tıklanınca çalışacaktır. 4. adımda düğme ve etiket panele eklenir. FlowLayout onları yan yana dizer. Yapıcı biter; panelin bileşen sayısı 2, düğmenin dinleyici sayısı 1'dir.

SayacPaneli nesnesisayac1etiketJLabelbileşen: 2JButton "Tıkla"dinleyici: 1lambda e -> {…}sayac, etiket alanlarına erişirekranda:Henüz tıklanmadı
Şekil 9.25: 5. adım. Kullanıcı tıkladı; EDT düğmeye kayıtlı dinleyicinin actionPerformed'ını, yani lambda gövdesini çağırdı.

5. adımda kullanıcı tıklar. EDT actionPerformed'ı, yani lambda gövdesini çağırır. sayac bir alan olduğu için artırılabilir: 1 olur. 6. adımda setText etiketin metnini “1 kez tıklandı” yapar ve Swing etiketi yeniden boyar. 7. ve 8. adımlar ikinci tıklamadır: aynı lambda yeniden çağrılır, alan 2 olur, etiket güncellenir.

Tıklamalar arasında program hiçbir kod çalıştırmaz; yalnız olay bekler. Bu davranış, konsol programının sürekli çalışan akışından çok farklıdır. Başsız kipte panel oluşturulup düğmenin doClick metoduyla iki tıklama verildiğinde etiket “1 kez tıklandı” ve “2 kez tıklandı” oldu. Düğme setEnabled(false) ile pasif yapılıp yeniden tıklandığında etiket değişmedi.

2 1 Henüz tıklanmadı
1 kez tıklandı
2 kez tıklandı
pasif: 2 kez tıklandı

9.8.3Anonim sınıf ve lambda

Aynı dinleyici iki biçimde yazılabilir. Klasik yazım anonim iç sınıftır:

dugme.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        sayac++;
        etiket.setText("Sayı: " + sayac);
    }
});

Java 8'den beri aynı iş bir lambda ifadesiyle yazılır:

dugme.addActionListener(e -> {
    sayac++;
    etiket.setText("Sayı: " + sayac);
});

// tek ifade ise süslü parantez gerekmez:
dugme.addActionListener(e -> System.exit(0));

Lambdanın kullanılabilmesinin nedeni ActionListener'ın yapısıdır. Arayüzün yalnız bir soyut metodu vardır.

Tanım 9.16

Fonksiyonel arayüz (functional interface), tek soyut metodu olan arayüzdür. Bir lambda ifadesi (lambda expression), böyle bir arayüzün tek metodunun gövdesi olarak kabul edilir: e -> …, actionPerformed(e)'nin gövdesidir.

Derleyici addActionListener'ın parametre tipine bakar, ActionListener'ı görür ve tek metodunu bulur. Lambdanın parametresi e, o metodun ActionEvent parametresi olur. Gövde tek bir ifadeyse süslü parantez de gerekmez: dugme.addActionListener(e -> System.exit(0));.

MouseListener'ın beş metodu olduğu için lambda ile yazılamaz. Derleyici hangi metodun gövdesinin yazıldığını bilemez:

Ana.java:9: error: incompatible types: MouseListener is not a functional interface
  p.addMouseListener(e -> System.out.println("tık"));
    multiple non-overriding abstract methods found in interface MouseListener

Fare olayları için bu yüzden adaptör sınıfı kullanılır.

Lambdanın bir sınırı daha vardır: içinden yerel değişken değiştirilemez. Lambda, kullandığı yerel değişkenin değerini yakalar (kopyalar); değişkenin kendisine bağlanmaz. Bu yüzden yerel değişken final ya da “fiilen final” olmalıdır, yani tanımlandıktan sonra hiç değiştirilmemelidir.

Sık yapılan hata

Sayacı yerel değişken olarak tutmak: int sayac = 0; … d.addActionListener(e -> sayac++);. Derleyici şu iletiyi verir:

Ana.java:10: error: local variables referenced from a lambda expression must be final or effectively final
  d.addActionListener(e -> sayac++);

Sayaç gibi değişen değerler sınıfın alanı (field) olmalıdır. Alanlar nesneye aittir; lambda nesneye bir referans tutar ve alanı değiştirebilir. Sayaç paneli bu yüzden private int sayac alanını kullanır.

9.8.4JLabel ve JTextField

JLabel bir etikettir. Metin ya da resim gösterir; kullanıcı onu değiştiremez. setText() metni, setIcon() resmi, setFont() yazı tipini, setForeground() metin rengini belirler.

JTextField tek satırlık metin alanıdır. Kullanıcının metin girmesi içindir; formlardan veri bu yolla toplanır. getText() ve setText() alanı okur ve yazar. setColumns() görünen sütun sayısını belirler. Bu sayı alanın tercih ettiği genişliği etkiler, alabileceği metnin uzunluğunu sınırlamaz. Başsız kipte 12 sütunluk alan 136, 24 sütunluk alan 268 piksel genişlik istedi. addActionListener() ile eklenen dinleyici, kullanıcı alanda Enter'a basınca çalışır.

JLabel etiket = new JLabel("Adınız:");
etiket.setFont(new Font("SansSerif", Font.BOLD, 14));
etiket.setForeground(Color.BLUE);
JTextField adAlani = new JTextField(12);
JLabel selam = new JLabel(" ");
adAlani.addActionListener(e -> selam.setText(
        "Merhaba, " + adAlani.getText() + "!"));

Kod iki etiket ve bir metin alanı oluşturur. Birinci etiket kalın, 14 puntoluk ve mavidir. Metin alanı 12 sütun genişliğindedir. İkinci etiket başta bir boşluk içerir: boş metinli bir etiketin tercih ettiği boyut 0 × 0'dır. Tek boşluklu etiket ise 3 × 16 piksel yer tutar ve selamın satırını baştan ayırır. Dinleyici, Enter'a basılınca alandaki metni okur ve selam etiketine yazar. Başsız kipte alana “Ayşe” yazılıp Enter olayı üretildiğinde selam etiketi Merhaba, Ayşe! oldu.

Lambda içinde adAlani ve selam yerel değişkenleri kullanılır. Bu değişkenler tanımlandıktan sonra değiştirilmediği için fiilen finaldir ve lambda onları yakalayabilir. Değişkenin gösterdiği nesnenin içeriği değişebilir; selam.setText(…) buna örnektir. Kural yalnız değişkenin başka bir nesneye yönlendirilmesini yasaklar.

9.8.5JTextArea ve JScrollPane

JTextArea çok satırlı metin alanıdır. İçeriği okunabilir, atanabilir ve güncellenebilir. new JTextArea(satır, sütun) ile görünür boyut önceden verilir. Sık kullanılan metotları şunlardır: getText(), setText(), append() (sona ekler), setEditable(false) (salt okunur yapar) ve setLineWrap(true) (uzun satırları kaydırır).

JScrollPane bir kaydırma bölmesidir. İçine konan bileşen (metin alanı, tablo, resim) ekrana sığmadığında kaydırma çubukları ekler. İçeriğin boyutu değişince kendini uyarlar. setPreferredSize() ile kendi boyutu, setViewportView() ile içeriği değiştirilir. Metin alanı kaydırma bölmesine yapıcıda verilir ve pencereye metin alanı değil, kaydırma bölmesi eklenir.

JTextArea alan = new JTextArea(5, 20);
alan.setText("Birinci satır\n");
alan.append("İkinci satır\n");
JScrollPane kaydirma = new JScrollPane(alan);
System.out.print(alan.getText());
System.out.println(alan.getLineCount());
Birinci satır
İkinci satır
3

setText eski içeriği siler ve yenisini yazar; append sona ekler. Metin "Birinci satır\nİkinci satır\n" olur. getLineCount() satır sonu sayısının bir fazlasını döndürür: metinde iki satır sonu vardır, sonuç 3'tür. Üçüncü satır boştur, ama imleç oraya konabildiği için sayılır. Boş bir metin alanında bile getLineCount() 1'dir.

Örnek 9.13

Aşağıdaki kod hangi üç satırı yazdırır?

JTextArea a = new JTextArea("x");
a.append("\ny");
a.append("\n");
System.out.println(a.getLineCount());
a.setText("tek");
a.append(" satır");
System.out.println(a.getText() + " " + a.getLineCount());
JTextField f = new JTextField(10);
f.setText(" 42 ");
System.out.println(Integer.parseInt(f.getText().trim()) + 1);

Metin "x\ny\n" olur: iki satır sonu, üç satır. İlk satır 3'tür. setText eski içeriği siler, append ekler: "tek satır", satır sonu yok, satır sayısı 1. getText() her zaman String döndürür. trim() baştaki ve sondaki boşlukları atar; 42 + 1 = 43. Boşluklar atılmasaydı parseInt şu istisnayı fırlatırdı: NumberFormatException: For input string: " 42 ".

3
tek satır 1
43

9.9Uygulama: metin dönüştürücü

Bu uygulama, bir metin alanındaki metni iki düğmeyle küçük ya da büyük harfe çevirir. Pencerenin düzeni iki yerleşim yöneticisinin birlikte kullanımına iyi bir örnektir:

  • Ortada (CENTER) bir JScrollPane içinde 8 satır × 50 sütunluk bir JTextArea vardır.
  • Altta (SOUTH) iki düğmeyi taşıyan bir JPanel vardır. Panel FlowLayout kullandığı için düğmeler ortalı ve yan yanadır.
  • pack() pencere boyutunu bu bileşenlerin tercih ettiği boyuttan hesaplar.
  • Düğmeler metni toLowerCase() ya da toUpperCase() ile dönüştürür ve alana geri yazar.
import java.awt.BorderLayout;
import javax.swing.*;

public class BuyukHarfeCevirici {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            var alan = new JTextArea("Java Programlama", 8, 50);
            var kucukHarfButton = new JButton("Küçük Harfe Çevir");
            var buyukHarfButton = new JButton("Büyük Harfe Çevir");
            kucukHarfButton.addActionListener(e ->
                    alan.setText(alan.getText().toLowerCase()));
            buyukHarfButton.addActionListener(e ->
                    alan.setText(alan.getText().toUpperCase()));
            var panel = new JPanel();
            panel.add(kucukHarfButton);
            panel.add(buyukHarfButton);
            var frame = new JFrame("BuyukHarfeCevirici");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.getContentPane().add(new JScrollPane(alan),
                    BorderLayout.CENTER);
            frame.getContentPane().add(panel, BorderLayout.SOUTH);
            frame.pack();
            frame.setVisible(true);
        });
    }
}

Bütün arayüz tek bir lambda içinde, EDT'de kurulur. Önce metin alanı ve iki düğme oluşturulur. Her düğmeye bir dinleyici eklenir. Dinleyici alanın metnini okur, dönüştürür ve sonucu alana geri yazar. Sonra düğmeler bir panele konur. Pencere oluşturulur, kaydırma bölmesi merkeze, düğme paneli güneye eklenir. pack() boyutu hesaplar ve pencere gösterilir.

Kodda üç ayrıntı vardır:

  • alan yerel bir değişkendir, ama lambda içinde kullanılabilir. Değişken tanımlandıktan sonra başka bir nesneye yönlendirilmez; fiilen finaldir. Lambda alanın kendisini değil, gösterdiği nesneyi kullanır ve o nesnenin metnini değiştirir.
  • toUpperCase() yeni bir String döndürür. String değiştirilemez olduğu için asıl metin değişmez; sonucu setText ile alana geri yazmak gerekir. alan.getText().toUpperCase(); tek başına yazılsaydı ekranda hiçbir şey değişmezdi.
  • BorderLayout.CENTER tam adıyla yazılır. Yalnız CENTER yazmak derlenmez, çünkü sabit BorderLayout sınıfının içindedir.

Başsız kipte aynı iki düğme ve metin alanı oluşturulup düğmelere doClick ile basıldığında metin önce büyük, sonra küçük harfe döndü:

JAVA PROGRAMLAMA
java programlama
varsayılan yerel ayar tr_TR: İSTANBUL / ENGLISH ISTANBUL / tr İSTANBUL

9.9.1Bir tıklamada neler olur?

Kullanıcının “Büyük Harfe Çevir” düğmesine tıklamasıyla ekranda metnin değişmesi arasında altı adım vardır.

  1. Kullanıcı düğmeye tıklar; işletim sistemi bunu Java'ya iletir.
  2. Java bir olay nesnesi oluşturur ve olay kuyruğuna koyar.
  3. EDT sıradaki olayı alır. Düğme, kayıtlı bütün ActionListener'ların actionPerformed'ını çağırır: lambdamız çalışır.
  4. alan.getText() mevcut metni okur.
  5. toUpperCase() yeni bir dizgi üretir; eski dizgi değişmez.
  6. setText alanın modelini günceller. Alan kendini repaint ile yeniden çizdirir. EDT sıradaki olaya geçer.
Kullanıcı tıklarBüyük Harfe ÇevirOlay kuyruğuActionEvent beklerEDTJButton olayı alırLambda: getText()"Java Programlama"toUpperCase()yeni StringsetText + repaintekran güncelleniralan: Java Programlama
Şekil 9.26: 3. adım. EDT olayı kuyruktan aldı ve düğmeye kayıtlı dinleyiciyi çağırdı.

6. adımda “model” sözcüğü geçer. Swing'de metin bileşenleri iki parçadan oluşur: metni tutan bir model ve onu ekranda gösteren bir görünüm. setText modeli değiştirir; görünüm bu değişikliği fark eder ve kendini yeniden çizdirir. Para üstü uygulaması bu ayrımı doğrudan kullanır.

9.9.2Büyük harf ve yerel ayar

toUpperCase() ve toLowerCase() parametresiz çağrılırsa bilgisayarın yerel ayarını (locale) kullanır. Türkçe, harf dönüşümünde özel kuralları olan dillerden biridir: noktalı ve noktasız iki ayrı “i” vardır. İngilizce ayarlı bir makinede "istanbul".toUpperCase() sonucu "ISTANBUL", Türkçe ayarlıda "İSTANBUL"'dur. Aynı program, çalıştığı bilgisayara göre farklı sonuç verir.

Sonucu sabitlemek için yerel ayar açıkça verilir: toUpperCase(Locale.forLanguageTag("tr")). Aşağıdaki deneme iki dilin kurallarını karşılaştırır.

String s = "Kış İzmir";
Locale tr = Locale.forLanguageTag("tr");
System.out.println(s.toUpperCase(tr));
System.out.println(s.toLowerCase(tr));
System.out.println(s.toUpperCase(Locale.ENGLISH));
System.out.println(s);
KIŞ İZMİR
kış izmir
KIŞ İZMIR
Kış İzmir

Türkçe kurallarında i → İ, ı → I, İ → i ve I → ı'dır. İngilizce kurallarında hem ı hem i büyüyünce I olur; İ ise olduğu gibi kalır. Bu yüzden üçüncü satır KIŞ İZMIR'dir. Son satır, dizgilerin değiştirilemezliğini bir kez daha gösterir: üç dönüşümden sonra s hâlâ "Kış İzmir"'dir.

Yerel ayar nesnesi eskiden new Locale("tr") ile oluşturulurdu. Bu yapıcı Java 19'dan beri kullanımdan kaldırılmıştır (deprecated); derleyici uyarı verir. Yerine Locale.forLanguageTag("tr") ya da Locale.of("tr") kullanılır.

Sık yapılan hata

Programın mantığını yerel ayara bağlı bir dönüşüme dayandırmak. Örneğin bir komut komut.toUpperCase().equals("QUIT") ile denetlenirse, Türkçe ayarlı makinede "quit" "QUİT" olur ve karşılaştırma başarısız olur. Kullanıcıya gösterilmeyen, program içi metinlerde Locale.ROOT ya da Locale.ENGLISH verilir; kullanıcıya gösterilen Türkçe metinlerde Türkçe yerel ayar verilir.

9.10Uygulama: para üstü hesaplayıcı

Bu uygulama, girilen kuruş miktarının en az sayıda madeni parayla (50, 25, 10, 5 ve 1 kuruş) nasıl ödeneceğini gösterir. Pencerede düğme yoktur: rapor her tuş vuruşunda kendiliğinden güncellenir. Bunu metin alanının belgesine eklenen bir DocumentListener sağlar.

9.10.1Alanlar ve yapıcı

public class ParaUstuHesaplama extends JFrame {
    private JTextField miktarAlani = new JTextField(12);
    private Document metin = miktarAlani.getDocument();
    private JTextArea rapor = new JTextArea(8, 40);

    public ParaUstuHesaplama() {
        var ustPanel = new JPanel();
        ustPanel.add(new JLabel("Miktar:"));
        ustPanel.add(miktarAlani);
        getContentPane().add(ustPanel, BorderLayout.NORTH);
        getContentPane().add(new JScrollPane(rapor),
                BorderLayout.CENTER);
        setBackground(Color.LIGHT_GRAY);
        rapor.setEditable(false);
        metin.addDocumentListener(new DocumentListener() {
            public void insertUpdate(DocumentEvent e) { raporuGuncelle(); }
            public void removeUpdate(DocumentEvent e) { raporuGuncelle(); }
            public void changedUpdate(DocumentEvent e) { raporuGuncelle(); }
        });
    }

Sınıf bu kez JPanel'den değil, JFrame'den türer. Pencerenin kendisi uygulamadır. Üç alan vardır: 12 sütunluk bir miktar alanı, bu alanın belgesi ve 8 satır × 40 sütunluk bir rapor alanı.

Swing'de veri ile görünüm ayrıdır. Bu düzene Model-Görünüm-Denetleyici (Model-View-Controller, MVC) denir. JTextField görünümdür: metni ekranda gösterir ve kullanıcının yazdıklarını alır. getDocument() ile alınan Document nesnesi ise metni tutan modeldir. Kullanıcı bir tuşa bastığında model değişir ve modeli dinleyen herkese haber verilir.

Yapıcı üst paneli kurar: bir “Miktar:” etiketi ve miktar alanı. Panel pencerenin kuzeyine, rapor alanı bir kaydırma bölmesiyle merkeze eklenir. Rapor salt okunur yapılır. Son olarak belgeye anonim bir DocumentListener eklenir.

DocumentListener'ın üç metodu vardır: insertUpdate (metin eklendi), removeUpdate (metin silindi) ve changedUpdate (stil değişti). Düz metin alanında stil olmadığı için changedUpdate hiç çağrılmaz. Ama arayüzün üç metodu da soyuttur ve üçü de yazılmalıdır. Üçü de aynı işi yapar: raporuGuncelle()'yi çağırır.

9.10.2Kod izleme: raporuGuncelle

void raporuGuncelle() {
    try {
        var miktar = Integer.parseInt(
                metin.getText(0, metin.getLength()));
        int[] dizi = {50, 25, 10, 5, 1};
        rapor.setText(miktar
                + " kuruş yapmak için kullanılacaklar:\n");
        for (int x : dizi) {
            rapor.append(miktar / x + " " + x + " kuruş\n");
            miktar %= x;
        }
    } catch (NumberFormatException e) {
        rapor.setText("Tam sayı değil veya aralık dışında");
    } catch (Exception e) {
        rapor.setText(e.toString());
    }
}

Metot belgenin metnini okur ve tam sayıya çevirir. metin.getText(0, metin.getLength()) belgenin 0. karakterinden başlayıp bütün uzunluğu kadar metni döndürür. Ardından para değerleri büyükten küçüğe bir diziye konur. Rapor setText ile sıfırdan başlatılır. Döngü her para değeri için iki iş yapar: kaç adet kullanılacağını (miktar / x) yazar ve kalan miktarı (miktar %= x) hesaplar.

Tablo 9.18: Miktar 93 iken raporuGuncelle izlemesi.
AdımSatırDeğişkenlerYeni çıktı
13: var miktar = Integer.parseInt(
4: metin.getText(0, metin.getLength()));
miktar=93—
25: int[] dizi = {50, 25, 10, 5, 1};miktar=93, dizi=[50, 25, 10, 5, 1]—
36: rapor.setText(miktar
7: + " kuruş yapmak için kullanılacaklar:\n");
miktar=93, dizi=[50, 25, 10, 5, 1]93 kuruş yapmak için kullanılacaklar:
49: rapor.append(miktar / x + " " + x + " kuruş\n");miktar=93, x=501 50 kuruş
510: miktar %= x;miktar=43, x=50—
69: rapor.append(miktar / x + " " + x + " kuruş\n");miktar=43, x=251 25 kuruş
710: miktar %= x;miktar=18, x=25—
89: rapor.append(miktar / x + " " + x + " kuruş\n");miktar=18, x=101 10 kuruş
910: miktar %= x;miktar=8, x=10—
109: rapor.append(miktar / x + " " + x + " kuruş\n");miktar=8, x=51 5 kuruş
1110: miktar %= x;miktar=3, x=5—
129: rapor.append(miktar / x + " " + x + " kuruş\n");miktar=3, x=13 1 kuruş
1310: miktar %= x;miktar=0, x=1—

Yöntem, açgözlü (greedy) bir algoritmadır: her adımda kullanılabilecek en büyük para seçilir. 93 kuruşta önce bir 50'lik kullanılır, 43 kalır. Sonra bir 25'lik, 18 kalır. Bir 10'luk, 8 kalır. Bir 5'lik, 3 kalır. Son olarak üç 1'lik. Toplam yedi madeni para. Tam sayı bölmesi adet hesabını, kalan işleci de kalan miktarı verir.

50kuruş× 125kuruş× 110kuruş× 15kuruş× 11kuruş× 3kalan miktar: 0
Şekil 9.27: 13. adım. Döngü bitti: 1 × 50, 1 × 25, 1 × 10, 1 × 5 ve 3 × 1 kuruş.

50, 25, 10, 5, 1 dizisi için açgözlü yöntem her zaman en az sayıda parayı verir. Bu her para sistemi için doğru değildir. Örneğin paralar 1, 3 ve 4 olsaydı, 6 için açgözlü yöntem 4 + 1 + 1 (üç para) seçerdi. Oysa 3 + 3 iki parayla aynı tutarı verir.

Örnek 9.14

Kullanıcı miktar alanına 187 yazdı. Rapor hangi satırları içerir?

187 / 50 = 3, kalan 37. 37 / 25 = 1, kalan 12. 12 / 10 = 1, kalan 2. 2 / 5 = 0, kalan 2. 2 / 1 = 2, kalan 0. Toplam 3 + 1 + 1 + 0 + 2 = 7 madeni para. Sıfır adetli satır da yazılır, çünkü döngü her para değeri için bir satır ekler:

187 kuruş yapmak için kullanılacaklar:
3 50 kuruş
1 25 kuruş
1 10 kuruş
0 5 kuruş
2 1 kuruş

9.10.3main ve olayların sırası

public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> {
        var frame = new ParaUstuHesaplama();
        frame.setTitle("Para Üstü Hesaplama");
        frame.pack();
        frame.setDefaultCloseOperation(
                JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    });
}

main pencereyi EDT'de oluşturur, başlığını verir, pack() ile boyutunu hesaplar ve gösterir. Programın asıl ilginç yanı, kullanıcının yazdıklarına göre hangi olayların geldiğidir. JFrame başsız kipte oluşturulamadığı için aynı alanlar, dinleyici ve raporuGuncelle metodu pencere olmadan bir sınıfta denendi; belgeye ekleme ve silme doğrudan yapıldı. Gelen olaylar şunlardır:

9 yazar              insert
3 yazar              insert
hepsini siler        remove
abc                  insert
99999999999          remove insert
setText 287          remove insert
setText boş          remove
boştan setText 287   insert
-7                   remove insert
68                   remove insert
187                  remove insert
boşluklu             remove insert
Tablo 9.19: Kullanıcı eylemleri, olaylar ve rapor (çalıştırılarak doğrulandı).
Kullanıcı / kodOlayRapor
9 yazarinsertUpdate9 kuruş: 0, 0, 0, 1 × 5, 4 × 1
3 yazar (alan 93)insertUpdate93 kuruş: 1, 1, 1, 1, 3
Hepsini silerremoveUpdateparseInt(""): “Tam sayı değil veya aralık dışında”
abc ya da 99999999999NumberFormatExceptionAynı ileti
setText("287") (alan doluyken)Önce removeUpdate, sonra insertUpdate5, 1, 1, 0, 2
setText("287") (alan boşken)Yalnız insertUpdate5, 1, 1, 0, 2
-7Geçerli sayı0, 0, 0, −1, −2: denetim eksik

Tablodaki her satır bir dersi taşır. Kullanıcı “93” yazarken iki olay gelir: “9” ve “3” için birer insertUpdate. Rapor her tuşta güncellenir; ara durum (9 kuruş) da bir an için görünür. Alan tamamen silinince metin boş dizgidir ve parseInt("") NumberFormatException fırlatır. İlk catch bunu yakalar.

99999999999 bir sayıdır, ama int'in en büyük değeri olan 2 147 483 647'yi aşar. parseInt bu durumda da NumberFormatException fırlatır; ileti bu yüzden “tam sayı değil veya aralık dışında” der. setText ise bir bütün olarak değil, iki adımda çalışır: önce eski metni siler, sonra yenisini ekler. Alan doluysa iki olay gelir. Birinci olayda alan bir an boştur ve rapor hata iletisini gösterir; ikinci olay doğru raporu yazar.

Negatif sayı ise bir eksikliği ortaya çıkarır. -7 geçerli bir tam sayıdır. Java'da tam sayı bölmesi sıfıra doğru keser ve kalanın işareti bölünenin işaretini alır: -7 / 5 = -1, -7 % 5 = -2. Rapor -1 5 kuruş ve -2 1 kuruş gibi anlamsız satırlar içerir:

-7 kuruş yapmak için kullanılacaklar:
0 50 kuruş
0 25 kuruş
0 10 kuruş
-1 5 kuruş
-2 1 kuruş

Program negatif değeri reddetmelidir. parseInt'ten hemen sonra bir denetim yeterlidir: if (miktar < 0) { rapor.setText("Miktar pozitif olmalı"); return; }.

İki catch bloğunun görevleri farklıdır. İlki beklenen hatayı, yani sayı olmayan metni ve int sınırını aşan değeri yakalar. İkincisi diğer her şey için genel bir güvencedir. Bu blok gereklidir, çünkü getText(0, n) kontrol edilen bir istisna olan BadLocationException'ı fırlatabileceğini bildirir. Derleyici bu istisnanın ele alınmasını ister; catch (Exception e) onu da kapsar.

Örnek 9.15

Para üstü programına 68 yazılınca rapor ne olur?

68 / 50 = 1, kalan 18. 18 / 25 = 0, kalan 18. 18 / 10 = 1, kalan 8. 8 / 5 = 1, kalan 3. 3 / 1 = 3. Rapor “68 kuruş yapmak için kullanılacaklar:” satırıyla başlar ve beş satırla devam eder:

68 kuruş yapmak için kullanılacaklar:
1 50 kuruş
0 25 kuruş
1 10 kuruş
1 5 kuruş
3 1 kuruş

9.11İyi uygulamalar

GUI programlarının kalitesini birkaç alışkanlık belirler.

Yerleşim yöneticisi kullanmak
Koordinatlar elle verilmez. Yerleşim yöneticisiyle düzen farklı ekran boyutu, çözünürlük ve yazı tipinde bozulmaz. Karmaşık düzenler iç içe panellerle kurulur.
Anlamlı adlar seçmek
b1, t2 yerine buyukHarfButton, miktarAlani gibi adlar kullanılır. Onlarca bileşenli bir arayüzde kod ancak böyle okunur kalır.
Olay güdümlü düşünmek
Tasarım “Kullanıcı şunu yapınca ne olmalı?” sorusuyla yapılır. Her eylem için bir dinleyici yazılır; dinleyiciler birbirinin sırasına güvenmez.
Arayüz ile iş mantığını ayırmak
Para üstü hesabı gibi kurallar ayrı bir metotta ya da sınıfta tutulur. Böylece arayüz olmadan test edilebilir ve başka bir arayüzde de kullanılabilir. Notta verilen çıktıların çoğu bu ayrım sayesinde pencere açılmadan elde edildi.
Arayüzü EDT'de kurmak
main'in içi tek bir çağrıdır: SwingUtilities.invokeLater(() -> { … }).
EDT'yi meşgul etmemek
Dinleyicide uzun bir iş (dosya indirme, büyük bir hesap) yapılırsa EDT o süre boyunca başka olay işleyemez ve pencere donar. Bu işler ayrı bir iş parçacığına taşınır; sonuç invokeLater ile EDT'ye geri verilir.
Örnek 9.16

Para üstü hesabını arayüzden ayırmak için nasıl bir metot yazılabilir?

Hesap, girdi olarak bir miktar alıp adetleri döndüren statik bir metoda taşınır:

static int[] paraUstu(int miktar) {
    int[] dizi = {50, 25, 10, 5, 1};
    int[] adet = new int[dizi.length];
    for (int i = 0; i < dizi.length; i++) {
        adet[i] = miktar / dizi[i];
        miktar %= dizi[i];
    }
    return adet;
}

raporuGuncelle artık yalnız metni okur, bu metodu çağırır ve sonucu rapora yazar. Metot ise bir konsol programında ya da bir testte doğrudan denenebilir: paraUstu(93) [1, 1, 1, 1, 3] döndürür.

9.12Çözümlü problemler

Örnek 9.17 (Problem 1)

BorderLayout kullanan bir panele önce A, sonra B düğmesi aynı NORTH bölgesine eklenirse kuzeyde hangisi görünür?

B. Her bölgede tek bileşen olur; son eklenen kazanır. A panelde kalır (bileşen sayısı 2'dir), ama yerleşime katılmaz. Başsız kipte 200 × 100'lük panelde B (0, 0, 200, 26), A ise 0 × 0 çıktı.

Örnek 9.18 (Problem 2)

Kullanıcı panele bir kez tıklayıp bırakırsa MouseListener'ın hangi metotları hangi sırayla çağrılır?

mousePressed → mouseReleased → mouseClicked. İşaretçi panele dışarıdan girerken ayrıca mouseEntered gelir. Basma ile bırakma arasında fare kıpırdarsa mouseDragged olayları araya girer ve mouseClicked gelmeyebilir.

Örnek 9.19 (Problem 3)

Color.RED.brighter() hangi rengi verir?

Yine (255, 0, 0). Kırmızı bileşen zaten en büyük değerdedir; 255 / 0,7 = 364 sınırda 255'e iner. Yeşil ve mavi bileşenler 0'dır ve 0 / 0,7 = 0 kalır.

Örnek 9.20 (Problem 4: hedef tahtası)

Panel boyutuna göre ölçeklenen, kırmızı-beyaz beş iç içe halkadan oluşan bir hedef tahtası çizilecektir. paintComponent nasıl yazılır? 300 × 200'lük panelde hangi çağrılar yapılır?

Merkez ve en büyük yarıçap işaretteki gibi hesaplanır. Döngü her turda yarıçapı beşte bir azaltır ve renkleri sırayla değiştirir:

int cx = getWidth() / 2, cy = getHeight() / 2;
int r = Math.min(cx, cy) - 5;
for (int i = 0; i < 5; i++) {
    int ri = r - i * r / 5;
    g.setColor(i % 2 == 0 ? Color.RED : Color.WHITE);
    g.fillOval(cx - ri, cy - ri, 2 * ri, 2 * ri);
}

300 × 200'de cx = 150, cy = 100, r = 95'tir. i * r / 5 soldan sağa hesaplanır: önce çarpma, sonra tam sayı bölmesi. Yarıçaplar 95, 76, 57, 38 ve 19 olur; çağrılar fillOval(55, 5, 190, 190), (74, 24, 152, 152), (93, 43, 114, 114), (112, 62, 76, 76) ve (131, 81, 38, 38)'dir. Daireler büyükten küçüğe çizilir. Küçükten büyüğe çizilseydi en son çizilen büyük daire hepsini örterdi.

Örnek 9.21 (Problem 5: sağ tıkla geri alma)

Çizim programında sağ tık son eğriyi silsin; sağ düğmeyle sürükleme de çizim yapmasın. Hangi değişiklikler gerekir?

mousePressed'ın başına bir denetim eklenir. Sağ düğmeyse son eğri silinir ve metot döner:

if (e.getButton() == MouseEvent.BUTTON3) {
    if (!egriler.isEmpty()) egriler.remove(egriler.size() - 1);
    repaint();
    return;
}

mouseDragged'ın başına da if (!SwingUtilities.isLeftMouseButton(e)) return; yazılır. Bu satır olmasaydı sağ düğmeyle sürüklemek, az önce silinen eğrinin yerine bir önceki eğriye nokta eklerdi. Liste boşken sağ sürükleme ise get(-1) ile IndexOutOfBoundsException: Index -1 out of bounds for length 0 fırlatırdı. Değişiklikler başsız kipte olay gönderilerek denendi: iki eğri çizildikten sonra sağ tık eğri sayısını 1'e indirdi ve sağ sürükleme hiçbir noktayı değiştirmedi.

Örnek 9.22 (Problem 6: sıcaklık dönüştürücü)

Bir JTextField'a °C girilip düğmeye basılınca °F gösterilsin. Dinleyici nasıl yazılır?

cevir.addActionListener(e -> {
    try {
        double c = Double.parseDouble(alan.getText());
        sonuc.setText(c * 9 / 5 + 32 + " °F");
    } catch (NumberFormatException ex) {
        sonuc.setText("Sayı değil");
    }
});

100 girildiğinde 212.0 °F, −40 girildiğinde -40.0 °F yazılır; iki ölçeğin eşit olduğu tek sıcaklık −40'tır. abc girildiğinde parseDouble NumberFormatException fırlatır ve “Sayı değil” görünür. c * 9 / 5 + 32 + " °F" ifadesinde sıra önemlidir: önce sayısal işlemler soldan sağa yapılır, en son dizgi eklenir. Lambdanın içindeki istisna parametresine e yerine ex adı verilir, çünkü e zaten lambdanın parametresidir.

Daha küçük iki alıştırma da aynı kalıpları kullanır. Sayaç penceresinde “−1”, “+1” ve “Sıfırla” düğmeleri bir private static int sayac alanını değiştirir ve etiketi günceller. İki “+1” ve bir “−1” tıklamasından sonra etiket 1, “Sıfırla”dan sonra 0 gösterir. Çizim programına “Temizle” düğmesi eklemek için düğme dinleyicisi egriler.clear(); repaint(); çalıştırır. Panel ve düğme pencerenin CENTER ve SOUTH bölgelerine konur.

9.13Sık yapılan hatalar

Grafik programlarında hataların bir kısmı derleyiciden gelir, bir kısmı ise programın sessizce yanlış davranmasıyla ortaya çıkar. Derleyici iletileri JDK 21 ile alınmıştır.

Tablo 9.20: Sık yapılan hatalar, belirtileri ve çözümleri.
HataBelirtiÇözüm
İçe aktarmayı unutmakcannot find symbol … class JFrameimport javax.swing.*;
setVisible(true) yokPencere hiç görünmezKurulumun sonunda setVisible(true)
EXIT_ON_CLOSE yokPencere kapanır, program arka planda sürersetDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
Metot adını yanlış yazmak (paintcomponent)Hiç çağrılmaz; @Override varsa method does not override or implement a method from a supertypeDoğru ad ve her zaman @Override
paintComponent'i public/protected olmadan yazmakattempting to assign weaker access privileges; was protectedprotected ya da public
super.paintComponent(g) unutmakZemin temizlenmez, eski çizim artıkları kalırGövdenin ilk satırı
Arayüzün bütün metotlarını yazmamak… is not abstract and does not override abstract method mouseExited(MouseEvent) in MouseListenerMouseAdapter'dan türetmek
Hareket dinleyicisini kaydetmemekmouseDragged hiç çağrılmazaddMouseMotionListener(…)
Son öğe için get(size())IndexOutOfBoundsException: Index 1 out of bounds for length 1get(size() - 1)
Lambda içinde yerel değişken değiştirmeklocal variables referenced from a lambda expression must be final or effectively finalDeğişkeni alan yapmak
Çok metotlu arayüze lambda vermekMouseListener is not a functional interfaceAdaptör ya da anonim sınıf
Sabiti sınıf adı olmadan yazmakcannot find symbol … variable CENTERBorderLayout.CENTER
java.awt.* ve java.util.* ile Listreference to List is ambiguousimport java.util.List;
EDT içinden invokeAndWaitjava.lang.Error: Cannot call invokeAndWait from the event dispatcher threadEDT'de doğrudan çağırmak ya da invokeLater
Dinleyicide uzun işPencere donar, tıklamalara yanıt vermezİşi ayrı iş parçacığına taşımak

İçe aktarma unutulduğunda derleyici aynı satır için iki hata verir: biri değişkenin tipi, biri new ifadesi içindir.

Ana.java:3: error: cannot find symbol
    JFrame f = new JFrame("x");
  symbol:   class JFrame
  location: class Ana
Ana.java:3: error: cannot find symbol
    JFrame f = new JFrame("x");
  symbol:   class JFrame
  location: class Ana

@Override notunun değeri en açık biçimde yanlış yazılmış metot adında görülür. Not olmadan paintcomponent (küçük c) yazan sınıf sorunsuz derlenir. Yeni bir metot tanımlanmıştır ve Swing onu hiç çağırmaz; panel boş kalır. Not eklenince derleyici hatayı hemen bulur:

Ana.java:7: error: method does not override or implement a method from a supertype
  @Override

paintComponent, JComponent'te protected olarak tanımlıdır. Geçersiz kılan metodun erişimi daraltılamaz: protected ya da public olabilir. Erişim belirteci hiç yazılmazsa metot paket erişimli olur ve derleyici reddeder:

Ana.java:7: error: paintComponent(Graphics) in Ana cannot override paintComponent(Graphics) in JComponent
  void paintComponent(Graphics g) {
  attempting to assign weaker access privileges; was protected

Sabitin sınıf adı olmadan yazılması da benzer bir iletidir:

Ana.java:9: error: cannot find symbol
  p.add(new JButton("x"), CENTER);
  symbol:   variable CENTER
  location: class Ana
Örnek 9.23

Bir çizim paneli derleniyor, ama eğriler görünmüyor, eski çizim artıkları kalıyor ve ilk sürüklemede istisna fırlıyor. Kodda şu satırlar var: egriler.get(egriler.size()).add(e.getPoint()) ve public void paintcomponent(Graphics g) { for (…) … }. Üç hata nedir?

Birincisi metodun adıdır: paintcomponent yeni bir metottur ve Swing onu çağırmaz. @Override eklenseydi derleme hatası alınırdı. İkincisi, metodun içinde super.paintComponent(g) yoktur; ad düzeltilince zemin temizlenmez ve artıklar kalır. Üçüncüsü son öğenin indisidir: get(size()) IndexOutOfBoundsException fırlatır, doğrusu size() - 1'dir. Kodun derlenmesi hatasız olduğunu göstermez; üç hatanın hiçbiri derleyiciye takılmaz.

9.14Özet

  • GUI programı olay güdümlüdür: main yalnız arayüzü kurar; akışı kullanıcının eylemleri belirler. Kodun büyük kısmı dinleyicilerden oluşur.
  • java.awt paketi Graphics, Color, Font, Point, yerleşim yöneticileri ve olay sınıflarını; javax.swing paketi adı J ile başlayan bileşenleri içerir. Swing bileşenleri Java'nın kendisi çizer.
  • Ekran koordinatlarında (0, 0) bileşenin sol üst köşesidir; x sağa, y aşağı doğru büyür.
  • Kalıcı çizim için JPanel'den türetilir ve paintComponent(Graphics g) geçersiz kılınır. Gövdenin ilk satırı super.paintComponent(g)'dir. Metodu Swing çağırır.
  • draw… metotları çerçeve, fill… metotları içi dolu şekil çizer. Elips ve daireler sınırlayıcı dikdörtgenle verilir: merkezi (cx, cy), yarıçapı r olan daire fillOval(cx − r, cy − r, 2r, 2r)'dir. drawString'in y'si taban çizgisidir.
  • Graphics nesnesi rengini ve yazı tipini hatırlar; setColor ve setFont sonraki bütün çizimleri etkiler. Sonradan çizilen şekil öncekini örter.
  • JFrame üst düzey penceredir. Boyut setSize ya da pack() ile verilir, kapatma davranışı setDefaultCloseOperation(EXIT_ON_CLOSE) ile belirlenir, en son setVisible(true) çağrılır. Varsayılan kapatma davranışı HIDE_ON_CLOSE'dur.
  • JPanel hem kapsayıcı hem tuvaldir; varsayılan yerleşim yöneticisi FlowLayout'tur. JFrame'in içerik bölmesi BorderLayout kullanır.
  • Point'in x ve y alanları public int'tir; getX() ve getY() double döndürür. translate göreli, setLocation mutlak taşır.
  • Color RGB üçlüsüyle (0–255) oluşturulur; değiştirilemezdir. darker() bileşenleri 0,7 ile çarpar, brighter() 0,7'ye böler; ikisi de sonucu tam sayıya keser ve birbirinin tam tersi değildir.
  • Font(ad, stil, punto); stil sabitleri bit bayraklarıdır: BOLD | ITALIC = 3. deriveFont(float) boyutu, deriveFont(int) stili değiştirir.
  • Yerleşim yöneticisi bileşenlerin konum ve boyutunu kurala göre hesaplar. add yalnız listeye ekler; yerleşim düzen hesaplanınca yapılır.
  • FlowLayout satır satır, ortalı ve 5 piksel boşlukla dizer. BorderLayout beş bölgeye yerleştirir: her bölgede tek bileşen, son eklenen kazanır, merkez kalanı alır. GridLayout eşit hücrelere böler. BoxLayout tek satır ya da tek sütun kurar.
  • Dinleyici arayüzü bir olayın metotlarını tanımlar; dinleyici add…Listener ile kaydedilir. MouseListener basma, bırakma, tıklama, giriş ve çıkışı; MouseMotionListener hareket ve sürüklemeyi bildirir. Kayıt arayüze göre yapılır.
  • Adaptör sınıfı (MouseAdapter) bütün metotları boş gövdeyle uygular; alt sınıf yalnız gerekeni yazar. Anonim iç sınıf tek kullanımlık dinleyici için uygundur.
  • Bir tıklama pressed → released → clicked üretir. MouseEvent konumu bileşene göre verir; getButton() düğmeyi (BUTTON3 sağ) söyler.
  • Swing tek iş parçacıklıdır: bileşenler yalnız EDT'de kurulur ve değiştirilir. invokeLater işi kuyruğa koyar ve döner; invokeAndWait iş bitene kadar bekler ve EDT içinden çağrılırsa Error fırlatır. Kuyruk sıralıdır.
  • Dinleyici ekrana çizmez; veriyi günceller ve repaint() ister. repaint() bir istektir; Swing uygun anda paintComponent'i çağırır.
  • ActionListener tek metotlu, fonksiyonel bir arayüzdür ve lambda ile yazılabilir. Lambda içinden yerel değişken değiştirilemez; değişen değerler alan olur.
  • JLabel metin gösterir, JTextField tek satır, JTextArea çok satır metin alır, JScrollPane kaydırır. getLineCount() satır sonu sayısının bir fazlasıdır.
  • toUpperCase() yerel ayara bağlıdır; Türkçede i → İ. Sonucu sabitlemek için yerel ayar açıkça verilir.
  • Metin bileşenlerinde model (Document) ile görünüm ayrıdır. DocumentListener her ekleme ve silmede çağrılır; setText dolu alanda iki olay üretir.
  • İyi uygulamalar: yerleşim yöneticisi kullanmak, anlamlı adlar seçmek, iş mantığını arayüzden ayırmak, arayüzü EDT'de kurmak ve EDT'yi uzun işlerle meşgul etmemek.

9.15Kavram listesi

9.16Kendini Dene

  1. g.drawRect(10, 20, 30, 40) ve g.fillRect(10, 20, 30, 40) çağrılarının boyadığı en sağ alttaki piksel hangisidir?

    Cevap

    drawRect için (40, 60), fillRect için (39, 59). Çerçeve, sınırlayıcı dikdörtgenin sağ ve alt kenarını da çizer ve en + 1 piksel geniş olur. Dolu dikdörtgen tam en × boy piksel boyar. İki değer de bir resim üzerinde boyalı pikseller sayılarak bulundu.

  2. new Color(10, 20, 30).brighter() hangi bileşenleri verir?

    Cevap

    (14, 28, 42). Her bileşen 0,7'ye bölünüp kesilir: 10 / 0,7 = 14,28 → 14; 20 / 0,7 = 28,57 → 28; 30 / 0,7 = 42,85 → 42. Hiçbir bileşen 255'e ulaşmadığı için sınır devreye girmez.

  3. 300 × 200 piksellik bir panel new BorderLayout(5, 5) kullanıyor. Kuzeye 30, güneye 20 piksel yüksekliği; batıya 60, doğuya 40 piksel genişliği tercih eden bileşenler ve merkeze bir bileşen ekleniyor. Merkez bileşeninin sınırları nedir?

    Cevap

    (65, 35, 190, 140). Merkez, batının sağından 5 piksel boşlukla başlar: x = 60 + 5. Kuzeyin altından 5 piksel boşlukla başlar: y = 30 + 5. Genişlik 300 − 60 − 40 − 2 × 5 = 190, yükseklik 200 − 30 − 20 − 2 × 5 = 140'tır. Batı ve doğu da aynı yükseklikte ve y = 35'tedir.

  4. Giriş yasak işaretinin paintComponent metodu 300 × 300 piksellik bir panelde çalışıyor. r, r2, bw ve bh kaçtır?

    Cevap

    r = min(150, 150) − 5 = 145. r2 = (int) 130,5 = 130. bw = (int) (130 × 1,4) = 182. bh = (int) (130 × 0,35) = (int) 45,5 = 45. Kesme yarımı da atar; yuvarlama olsaydı 46 bulunurdu.

  5. Para üstü programına 41 yazılınca rapordaki adetler nelerdir?

    Cevap

    0 × 50, 1 × 25, 1 × 10, 1 × 5, 1 × 1. 41 / 50 = 0 (kalan 41), 41 / 25 = 1 (kalan 16), 16 / 10 = 1 (kalan 6), 6 / 5 = 1 (kalan 1), 1 / 1 = 1. Toplam dört madeni para.

  6. Aşağıdaki kod derlenir mi?

    String ad = " Ayşe ";
    ad = ad.trim();
    JButton b = new JButton("x");
    b.addActionListener(e -> System.out.println(ad));
    Cevap

    Derlenmez: local variables referenced from a lambda expression must be final or effectively final. Lambda ad'ı yalnız okuyor, ama ad ikinci satırda yeniden atandığı için fiilen final değildir. Çözüm, kırpılmış değeri yeni bir değişkene almaktır: String temiz = ad.trim(); ve lambda içinde temiz.

  7. Yalnız mouseClicked'ı geçersiz kılan bir MouseAdapter nesnesi panele addMouseMotionListener ile kaydediliyor. Panele tıklanınca metot çalışır mı?

    Cevap

    Çalışmaz. MouseAdapter üç arayüzü birden uyguladığı için kayıt derlenir, ama panel bu nesneyi yalnız hareket dinleyicisi olarak tanır. Tıklama olayları yalnız addMouseListener ile kaydedilen dinleyicilere gider. Panele bir tıklama olayı gönderildiğinde metot 0 kez çağrıldı.

  8. Metni "a\n\nb\n" olan bir JTextArea'da getLineCount() kaç döndürür?

    Cevap

    4. Metinde üç satır sonu vardır: “a”, boş satır, “b” ve son satır sonundan sonraki boş satır. Satır sayısı, satır sonu sayısının bir fazlasıdır.

9.17İleri okuma

  • Oracle, The Java Tutorials, “Creating a GUI With Swing”: bileşenlerin, yerleşim yöneticilerinin ve olay dinleyicilerinin resmî ve ücretsiz anlatımıdır. “Concurrency in Swing” dersi olay iş parçacığını ayrıntılı açıklar.
  • Oracle, The Java Tutorials, “Performing Custom Painting”: paintComponent, repaint ve kırpmanın nasıl birlikte çalıştığını adım adım bir örnekle gösterir.
  • C. S. Horstmann, Core Java, Volume I: Fundamentals (12. baskı), Bölüm 10–11: grafik programlama, olay işleme ve Swing bileşenlerini MVC bakışıyla işler.
  • E. Gamma, R. Helm, R. Johnson ve J. Vlissides, Design Patterns: Gözlemci deseninin kaynak anlatımıdır; Swing dinleyicilerinin neden böyle tasarlandığını açıklar.

9.18Kaynaklar

  1. Oracle, The Java Tutorials: Creating a GUI With Swing; Performing Custom Painting; Laying Out Components Within a Container; Writing Event Listeners; Concurrency in Swing.
  2. Oracle, Java SE 21 API Documentation: java.awt.Graphics, java.awt.Color, javax.swing.JFrame, javax.swing.SwingUtilities.
  3. P. Deitel ve H. Deitel, Java: How to Program, 11. baskı, Pearson (çevrim içi bölümler: Swing GUI Components; Graphics and Java 2D).
  4. K. Sierra, B. Bates ve T. Gee, Head First Java, 3. baskı, O'Reilly (Bölüm 14–15: GUI, Swing ve olaylar).
  5. C. S. Horstmann, Core Java, Volume I: Fundamentals, 12. baskı, Pearson (Bölüm 10–11: grafik programlama, olay işleme, Swing).
  6. A. B. Downey ve C. Mayfield, Think Java, 2. baskı, O'Reilly (Ek C: Graphics).
  7. E. Gamma, R. Helm, R. Johnson ve J. Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1994.

İlgili sayfalar: Bölüm 9 sunumu · Çalışma soruları · Swing Yerleşim Yöneticileri simülatörü · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.