Jak korzystać z Timer'ów

0

Ponieważ to mój pierwszy post tutaj, na początek witam wszystkich i zgłaszam się z prośbą o pomoc.

Dla wyjaśnienia mojego problemu napisałem prostą aplikację wzorowaną na przykładzie z książki "Java podstawy". Tworzy ona okno z dodanym komponentem i na każde kliknięcie reaguje dodaniem/narysowaniem kwadratu w miejscu kliknięcia. Ostatnio dodany kwadrat jest wyróżniony wypełnieniem.
Kod programu: listing1 na końcu post'a.

Co chciałbym uzyskać: chciałbym, aby wyróżnienie ostatniego kwadratu stopniowo zanikało, co teoretycznie udało mi się uzyskać (listing2), ale jest problem, którego nie potrafię rozwiązać. Timer, który wywołuje obiekt klasy FillFadeHandler zmniejszający wartosc alpha dla koloru wypełnienia, działa w nieskończoność. Zauważalne to jest, jeśli porówna się szybkość, z jaką maleje wartość zmiennej iFillAlpha w przypadku gdy na komponencie jest jeden kwadrat, lub wiele kwadratów.

Czyli rozwiązanie którego potrzebuję, to możliwość zakończenia działania Timera w dwóch sytuacjach:

  • gdy zmniejszana wartość zmiennej iFillAlpha dojdzie do 0
  • gdy nastąpi kolejne kliknięcie tworzące nowy Timer
    Jednak nie wiem, gdzie i w jaki sposób należy zakończenie Timer'ów wstawić.

Z góry dziękuję za wszelką pomoc.
PS. Jeśli w prezentowanym kodzie są rzeczy zrobione w sposób nieprawidłowy, też sie chętnie o tym dowiem. W tematyce Javy dopiero startuję, więc sporo jest ekspetymentów ;)

listing1 TimerTest1.java:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;

public class TimerTest1
	{
	public static void main(String[] args)
		{
		EventQueue.invokeLater(
			new Runnable()
				{
				public void run()
					{
					JFrame oOkno = new JFrame();
					oOkno.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
					oOkno.setMinimumSize(new Dimension(640, 480));
					oOkno.setPreferredSize(new Dimension(640, 480));
					oOkno.setLocationRelativeTo(null);
					
					TimerComponent oComponent = new TimerComponent();
					oOkno.add(oComponent);
					
					oOkno.setVisible(true);
					}
				}
			);
		}
	}

class TimerComponent
	extends JComponent
	{
	private static final int DLUGOSC_BOKU = 10;
	private ArrayList<Rectangle2D> aKwadraty;
	private Rectangle2D oOstatniDodany;
	private class MouseHandler
		extends MouseAdapter
		{
		@Override public void mousePressed(MouseEvent event)
			{
			dodaj(event.getPoint());
			repaint();
			}
		}
	public TimerComponent()
		{
		aKwadraty = new ArrayList<Rectangle2D>();
		oOstatniDodany = null;
		addMouseListener(new MouseHandler());
		}
	@Override public void paintComponent(Graphics g)
		{
		Graphics2D g2 = (Graphics2D)g;
		g2.drawString("Ilość: " + aKwadraty.size(), 3, 12);
		for (Rectangle2D oKwadrat: aKwadraty)
			{
			if (oKwadrat == oOstatniDodany)
				{
				g2.setColor(new Color(255, 0, 0));
				g2.fill(oKwadrat);
				g2.setColor(new Color(0, 0, 0));
				}
			g2.draw(oKwadrat);
			}
		}
	public void dodaj(Point2D oPunkt)
		{
		oOstatniDodany = new Rectangle2D.Double(oPunkt.getX() - DLUGOSC_BOKU / 2, oPunkt.getY() - DLUGOSC_BOKU / 2, DLUGOSC_BOKU, DLUGOSC_BOKU);
		aKwadraty.add(oOstatniDodany);
		}
	}

listing2 TimerTest2.java

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;

public class TimerTest2
	{
	public static void main(String[] args)
		{
		EventQueue.invokeLater(
			new Runnable()
				{
				public void run()
					{
					JFrame oOkno = new JFrame();
					oOkno.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
					oOkno.setMinimumSize(new Dimension(640, 480));
					oOkno.setPreferredSize(new Dimension(640, 480));
					oOkno.setLocationRelativeTo(null);
					
					TimerComponent oComponent = new TimerComponent();
					oOkno.add(oComponent);
					
					oOkno.setVisible(true);
					}
				}
			);
		}
	}

class TimerComponent
	extends JComponent
	{
	private static final int DLUGOSC_BOKU = 10;
	private ArrayList<Rectangle2D> aKwadraty;
	private Rectangle2D oOstatniDodany;
	private int iFillAlpha;
	private class FillFadeHandler
		implements ActionListener
		{
		public void actionPerformed(ActionEvent oEvent)
			{
			if (iFillAlpha > 0)
				{
				--iFillAlpha;
				repaint();
				}
			}
		}
	private class MouseHandler
		extends MouseAdapter
		{
		@Override public void mousePressed(MouseEvent event)
			{
			dodaj(event.getPoint());
			repaint();
			Timer oTimer = new Timer(20, new FillFadeHandler());
			oTimer.start();
			}
		}
	public TimerComponent()
		{
		aKwadraty = new ArrayList<Rectangle2D>();
		oOstatniDodany = null;
		iFillAlpha = 255;
		addMouseListener(new MouseHandler());
		}
	@Override public void paintComponent(Graphics g)
		{
		Graphics2D g2 = (Graphics2D)g;
		g2.drawString("Ilość: " + aKwadraty.size(), 3, 12);
		g2.drawString("FillAlpha: " + iFillAlpha, 3, 24);
		for (Rectangle2D oKwadrat: aKwadraty)
			{
			if (oKwadrat == oOstatniDodany)
				{
				g2.setColor(new Color(255, 0, 0, iFillAlpha));
				g2.fill(oKwadrat);
				g2.setColor(new Color(0, 0, 0));
				}
			g2.draw(oKwadrat);
			}
		}
	public void dodaj(Point2D oPunkt)
		{
		oOstatniDodany = new Rectangle2D.Double(oPunkt.getX() - DLUGOSC_BOKU / 2, oPunkt.getY() - DLUGOSC_BOKU / 2, DLUGOSC_BOKU, DLUGOSC_BOKU);
		aKwadraty.add(oOstatniDodany);
		iFillAlpha = 255;
		}
	}
0

Można zakończyć działanie tego timera przez jego metodę stop().
Powinno się ją wstawić w tym miejscu gdzie dowiadujesz się, że kolejne zmiany są już niepotrzebne. Czyli najlepiej tam gdzie sprawdzasz warunek zakończenia animacji. W tym przypadku tam gdzie sprawdzasz, że iFillAlpha jest mniejsza od zera. Czyli w actionPerformed().

Zauważ, że skoro oznaczyłeś, że co 20 ms ma być wyzwalana jedna zmiana kanału alfa, a zmian tych jest dla każdego kwadratu 256, to daje to czas trwania sekwencji animacji równy 5120 ms, czyli ponad 5 sekund na każdą zmianę przezroczystości. Jeżeli ten czas jest u Ciebie krótszy, to tylko z powodu innych błędów w tym kodzie, o czym poniżej.

Teraz co jest błędami.

  1. Najpoważniejszy z nich to tworzenie nowego obiektu licznika (timera) oraz natychmiastowe zgubienie jego referencji bo zmienna referencyjna oTimer ginie od razu po dojściu do końca bloku (ale sam obiekt licznika "żyje" o wiele dłużej).
  2. Drugim błędem tego samego typu jest każdorazowe tworzenie nowego obiektu FillFadeHandler w przy tworzeniu nowego licznika. W efekcie przy szybkim wciskaniu system tworzy Ci całą masę liczników (anonimowych), które są przecież niczym innym niż nowymi wątkami oraz taką samą masę obiektów FillFadeHandler, które masowo żądają dostępu do tej samej zmiennej iFillAlpha i zmniejszają ją o wiele szybciej niż raz na 20 ms. Im więcej w tym samym czasie odpali się liczników tym szybciej zmniejszają i tym krócej trwa animacja. Jeżeli po sobie odpalisz szybko 10 kwadratów, to wyzwolisz 10 liczników, które co 20 ms zmniejszą iFillAlpha łącznie o 10, a nie o 1 jak zapewne chciałeś. W efekcie EventDispatchThread musi wykonać 10 procedur actionPerformed zamiast jednej. Gdyby Swing był wielowątkowy, to może by nie dało się tego zauważyć, ale wtedy zmienna iFillAlpha zostałaby uszkodzona przez konkurujące ze sobą wątki z powodu braku jej ochrony (braku synchronizacji). W obu wypadkach byłoby źle.

Co zrobić? Kiedy licznik animacji dobiegnie do zera powinieneś zatrzymać licznik oraz ubić go (java zrobi to sama gdy nie będzie referencji do niego).
Powinieneś kończyć i zabijać poprzedni licznik również gdy pojawia się nowe wciśnięcie myszy, następnie tworzyć nowy licznik i rozpoczynać jego bieg.
Wynika z tego, że w jednym momencie musi działać zaledwie jeden licznik na raz.
Dzięki temu oraz ponieważ Timer Swinga ma możliwość działania wielokrotnego, to prościej jest ustawić mu setRepeats(true), a następnie uruchamiać go i zatrzymywać zależnie od potrzeby dla kolejnych kwadratów.
Z tego wynika kolejna rzecz - actionPerformed oraz mousePressed mogłyby operować jednym i tym samym licznikiem (timerem), więc najlepiej zgrupować je w jednej klasie EventHandler i umieścić timer jako jej pole.

Jest coś jeszcze, co nie jest błędem (i nie ma znaczenia dla wydajności w tak małym przykładzie), ale w programach bawiących się w rozbudowane animacje ma znaczenie. Otóż najszybszą pamięcią jest pamięć obrazu, wolniejszą jest pamięć RAM z dostępem przez stos (zmienne lokalne), a najwolniejszą RAM z dostępem przez stertę (obiekty).
Jeżeli robi się jakiekolwiek animacje, to należy unikać operacji new w trakcie działania tych animacji ponieważ uzależnia się video RAM (to co widać) od szybkości sterty. Dlatego jedną z ważniejszych rzeczy jest wcześniejsze przygotowanie obiektów, które w procedurach paint mogą być już tylko odczytywane, a nie tworzone i obliczane. W przypadku tak malutkiego programu można (dla wprawy) utworzyć kolory z wszystkimi niezbędnymi poziomami alfa, a w paintComponent jedynie ich używać. Podobnie zawsze warto używać gotowych stałych obiektów kolorów niż tworzyć je na żądanie.

Oto przerobiony nieco kod robiący to co należy. Przy okazji usunąłem wszelkie manipulacje double, które na ekranie rastrowym są niczym innym jak tylko marnowaniem mocy obliczeniowej (z niewiadomego dla mnie powodu autorzy piszący książki o javie uczą wybitnie niewydajnego programowania grafiki 2D).

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.Timer;

public class TimerTest2
{
	public static void main(String[] args)
	{
		EventQueue.invokeLater(new Runnable()
		{
			@Override public void run()
			{
				JFrame oOkno = new JFrame();
				oOkno.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
				oOkno.setMinimumSize(new Dimension(640, 480));
				oOkno.setPreferredSize(new Dimension(640, 480));
				oOkno.setLocationRelativeTo(null);
				oOkno.add(new TimerComponent());
				oOkno.setVisible(true);
			}
		});
	}
}

class TimerComponent extends JComponent
{
	private static final int DLUGOSC_BOKU = 50;	//pikseli
	private static final int OKRES_ZMIANY = 20;	//ms
	private static final int KROK_ZMIANY = 8;	//jedna zmiana: 8/256

	public TimerComponent()
	{	//przygotowanie gotowych kolorów
		for(int i = 0; i < kolory.length; ++i)
			kolory[i] = new Color(255, 0, 0, i);
		resetAlpha();
		addMouseListener(new EventHandler()); //istnieje tylko jeden
	}

	private class EventHandler extends MouseAdapter implements ActionListener
	{
		private EventHandler()
		{	
			oTimer = new Timer(OKRES_ZMIANY, this);
			oTimer.setRepeats(true);
		}

		@Override public void mousePressed(MouseEvent event)
		{
			dodaj(event.getPoint());
			resetAlpha(); //wymusi iFillAlpha > 0 w actionPerformed
			if(!oTimer.isRunning())	//gdy zakończona seria actionPerformed
				oTimer.start();		//wymusi kolejną serię
		}

		@Override public void actionPerformed(ActionEvent oEvent)
		{
			if(iFillAlpha > 0)
			{
				if((iFillAlpha -= KROK_ZMIANY) < 0)
				{
					iFillAlpha = 0;
					oTimer.stop();
				}
				repaint();
			}
		}
		private Timer oTimer;
	}

	@Override public void paintComponent(Graphics g)
	{
		super.paintComponent(g); //ewentualne odrysowanie tła
		Graphics2D g2 = (Graphics2D)g;
		g2.drawString("Ilość: " + aKwadraty.size(), 3, 12);
		g2.drawString("FillAlpha: " + iFillAlpha, 3, 24);
		for(Rectangle oKwadrat: aKwadraty)
		{
			if(oKwadrat == oOstatniDodany)
			{
				Color poprzedni = g2.getColor();
				g2.setColor(kolory[iFillAlpha]);
				g2.fill(oKwadrat);
				g2.setColor(poprzedni);
			}
			g2.draw(oKwadrat);
		}
	}

	public void dodaj(Point oPunkt)
	{
		oOstatniDodany = new Rectangle(oPunkt.x - (DLUGOSC_BOKU >> 1),
			oPunkt.y - (DLUGOSC_BOKU >> 1), DLUGOSC_BOKU, DLUGOSC_BOKU);
		aKwadraty.add(oOstatniDodany);
	}

	private void resetAlpha() { iFillAlpha = 255; }

	private ArrayList<Rectangle> aKwadraty = new ArrayList<Rectangle>();
	private Rectangle oOstatniDodany = null;
	private int iFillAlpha;
	private Color[] kolory = new Color[256];
}
0

Witam, tutaj autor tematu (w międzyczasie założyłem konto)

Przyznam, że już powoli zaczynałem mieć obawy, że mój wątek pozostanie bez odpowiedzi, jednak Twój post przerósł moje oczekiwania, za co jestem bardzo wdzięczny - wyjaśniłeś mi dużo więcej, niż prosiłem w pytaniu.

Metodę stop() namierzyłem w dokumentacji, ale właśnie nie miałem pomysłu na to, jak w metodzie zmniejszającej wartość alpha, lub metodzie tworzącej Timer dobrać się do poprzedniego Timera. Jednak już po przeczytaniu o gubieniu referencji w opisie do pierwszego błędu zaiskrzyło i faktycznie uświadomiłem sobie, że rozwiązanie które pokazałeś, jest proste i logiczne :)
Po Twoich wyjaśnieniach mógłbym już zrobić usuwanie Timera i tworzenie nowego z nowym FillHandler, ale zaprezentowany sposób z jednorazowym utworzeniem Timer'a a następnie tylko uruchamianiem i zatrzymywaniem go nawet odpowiada mi dużo bardziej, niż to, o co prosiłem w pierwszym poście. :)

W kwestii korzystania z Rectangle2D.double - fakt, ze wzoruję się na jednej z książek o Javie, gdzie przedstawione były, jako znaczne zwiększenie możliwości, choć nie miałem jeszcze okazji tego odczuć ;) (sugerowano np, że można za jednostki przyjąć coś bardziej intuicyjnego, np centymetry, które następnie będą podczas rysowania konwertowane na pixele)

W kwestii szybkości pamięci. Wiedziałem o tym, że tworzenie obiektów na stercie ma narzut czasowy związany z pamięcią o nieco wolniejszym dostępie, jednak nie wyciągnąłem dalszych wniosków, że powinno się w miarę możliwości ograniczyć ich tworzenie w takich miejscach, więc za tę informację także dziękuję - na pewno przyda się na przyszłość.

Dalej w temacie wydajności to, co najbardziej podoba mi się w Twoim rozwiązaniu, to ilość tworzonych obiektów po każdym kliknięciu na komponencie.
U mnie jest to: 1 nowy kwadrat, 1 nowy timer, 1 nowy FillHandler, 256 obiektów kolorów (a nawet więcej, gdyż Timer nie zatrzymywał się po dojściu fillAlpha do 0).
U Ciebie: 1 nowy kwadrat
Fakt, że z powyższego porównania widać, że w moich programach dosyć mocno spamuje obiektami, co pewnie pozytywnie na wydajność wpływać nie będzie. Niby GC po mnie posprząta (jest to spore ułatwienie, dzięki któremu nie muszę się martwić zgubionymi obiektami, choć przyznam, że brakuje mi usuwania niepotrzebnych obiektów w stylu C++), ale w moim przypadku GC pewnie będzie uruchamiany dużo częściej niż w Twoim, a każde jego uruchomienie spowoduje jakiś narzut czasowy.
Powstaje sporo takiego kodu jak mój, a potem ludzie narzekają, że soft w Javie działa wolno ;) muszę sobie wyrobić nawyki do kodowania w taki sposób, jak zaprezentowałeś

Na koniec jeszcze miałbym pytanie o zmiany, które wprowadziłeś w sekcji import.
Jakiś czas temu sam się zastanawiałem nad tym, czy ma wpływ na rozmiar/wydajność programu to, czy importuję tylko niezbędne klasy, czy całe pakiety. Wykonałem kompilację Twojego przykładu, a następnie zmieniłem import'y na całe pakiety i ponowna kompilacja utworzyła pliki *.class o dokładnie takim samym rozmiarze, choć generują różne hashe md5, więc jakaś różnica jest.
Więc czy importowanie tylko niezbędnych klas, to jedynie kwestia stylu programowania, czy kryje się za tym coś więcej?

0

W przypadku Timera są dwie klasy o identycznych nazwach: java.util.Timer oraz java.swing.Timer. Używanie gwiazdek w tym wypadku pozostawia wątpliwość co do tego której klasy się używa. Szczególnie w sytuacji gdyby istniał również import java.util.*.

Jest też i drugi powód wrzucania pełnych nazw klas. Chodzi o to, że jednym rzutem oka widać wtedy od jak wielu innych klas zależy ta, której plik się przegląda. Ilość i rodzaj tych odwołań mówi wiele o tym jakiego rodzaju klasę się przegląda - czy jest ona zależna od wielu składników i czy te składniki są systemowe czy też są to klasy obce. Zasadniczo jeżeli lista importów przekracza ze 20 pozycji, to należałoby się zastanowić czy projekt klasy jest dobry i czy nie należałoby jej podzielić na mniejsze. Chodzi o to, że im więcej jest zależności tym więcej potencjalnie słabych punktów. Importowanie z gwiazdkami zamazuje tę informację dając złudzenie, że wszystko jest OK, nawet gdy klasa odwołuje się do podejrzanie wielu rzeczy.

Jest też i trzeci powód używania konkretnych nazw klas. Chodzi o zależności tworzące cykle. Czyli klasa A jest zależna od B, B zależna od C, a C znowu zależy od A. Jeżeli takie coś powstanie, to niemal na pewno oznacza to błędny projekt (fragmentu) aplikacji. Wychwycenie cykli z importami i gwiazdkami jest niemal niemożliwe.

Jedynym chyba powodem importów z gwiazdką jest (złudna) wygoda w pisaniu mikroskopijnych aplikacji lub naprawdę małych apletów. Dzisiaj apletów prawie się nie pisze, więc plusy z takiego podejścia są znikome, a minusy, które podałem wyraźnie przeważają. Weź pod uwagę, że koszt wypisania listy importów jest w porównaniu z nagłówkiem C++ bardzo niewielki, a zastosowanie zasadniczo to samo.
Jeszcze prościej jest używając jakiegoś IDE bo importy są generowane automatem w wyniku wybrania elementu jakiejś klasy podczas pisania, tak samo automatycznie są też usuwane. Koszt jest więc żaden, a zalety są.

Co do braku operacji delete oraz destruktora. Są one potrzebne w C++ tylko z tego powodu, że muszą w prymitywny sposób zastępować śmieciarkę. Problem wynika z zupełnie innego podejścia do obiektów. W C++ obiekt, to "miejsce w pamięci" zupełnie niezależne od referencji do niego. W Javie referencje determinują istnienie obiektu. Wywołanie jawnej metody usuwającej zasoby obce i przypisanie zmiennej referencyjnej nulla lub referencji innego obiektu jest funkcjonalnie tym samym co wywołanie delete oraz destruktora - tyle, że pozornie w innej kolejności. Chodzi o to, że jeżeli chcesz z obiektu usunąć zasoby, nad którymi środowisko programu nie panuje (np. wyłączyć urządzenie lub usunąć scenę 3D), to obiekt musi jeszcze istnieć. Naturalne jest więc, że powinno się to wykonywać zanim obiekt zostanie zniszczony - i tak właśnie dzieje się w Javie. Nie ma potem znaczenia po usunięciu ostatniej referencji czy pamięć obiektu będzie istniała długo czy krótko bo to będzie już interes śmieciarki przy defragmentacji sterty.
Natomiast w C++ występuje pewien absurd do którego wszyscy się przyzwyczaili: Delete kasuje obiekt (jako jego pamięć), ale zanim skończy (a raczej zanim zacznie) kasować wywołuje destruktor, który czyści zasoby (lub całe ich stadko). Jest to mało naturalne bo obiekt niby kasowany, a jeszcze istnieje. Czasem dochodzi do jeszcze większych paradoksów gdy trzeba jawnie wywołać destruktor. Wtedy faktycznie destrukcja następuje dwukrotnie, co może przyprawić o ból głowy z ustalaniem czy zasoby zostały już zwolnione.

Najciekawsze zjawisko pojawiło się ostatnio w C++. Każdy większy framework (nawet silnik gry) posługuje się jakąś własną wersją śmieciarki i "inteligentnymi wskaźnikami". Jest to dokładnie to samo co java ma już od dawna i to wbudowane w język. W efekcie zamyka to usta wszelkim przeciwnikom javy, śmieciarek oraz marudzeniu o utracie wydajności.

ps. Można jeszcze ulepszyć obsługę wyświetlania przez ograniczenie zakresu odmalowywania do obszarów naprawdę zmienianych przez kod obsługi zdarzeń. Chodzi o to, że po kliknięciu jedyne co naprawdę powinno się odmalowywać, to obszar ostatniego kwadratu, obszar poprzedniego kwadratu gdy kliknięcie nastąpiło przed zakończeniem jego animacji oraz obszar napisów. Cała reszta wraz ze stadem innych kwadratów powinna się odmalowywać jako żądanie Swinga lub systemu, wiec nie potrzeba się tym zajmować:

	private class EventHandler extends MouseAdapter implements ActionListener
	{
		private EventHandler()
		{	
			oTimer = new Timer(OKRES_ZMIANY, this);
			oTimer.setRepeats(true);
		}

		@Override public void mousePressed(MouseEvent event)
		{
			if(oTimer.isRunning()) //przerwanie animacji
				poprzedni = oOstatniDodany; //odrysowanie przedostatniego
			else //gdy zakończona seria actionPerformed
				oTimer.start();	//wymusi kolejną serię
			dodaj(event.getPoint());
			resetAlpha(); //wymusi iFillAlpha > 0 w actionPerformed
		}

		@Override public void actionPerformed(ActionEvent oEvent)
		{
			if(iFillAlpha > 0)
			{
				if((iFillAlpha -= KROK_ZMIANY) < 0)
				{
					iFillAlpha = 0;
					poprzedni = null;
					oTimer.stop();
				}
				repaint(0, 0, 50, 30); //dla napisów (powinno się obliczyć wymiary)
				if(poprzedni != null) //wygaszenie poprzedniego kwadratu
					repaint(poprzedni.x, poprzedni.y,
						poprzedni.width + 1, poprzedni.height + 1);
				//dla aktywnego kwadratu
				repaint(oOstatniDodany.x, oOstatniDodany.y,
					oOstatniDodany.width + 1, oOstatniDodany.height + 1);
			}
		}
		private Timer oTimer;
		private Rectangle poprzedni;
	}
0

Dzięki wielkie za wyjaśnienie tego i przedstawienie sposobu odrysowywania fragmentów komponentu. Wszystko mi sie bardzo przyda :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1