Niedziałający wątek w stoperze

0

Zaczynam się uczyć wątków i chcę zrobić stoper. Niestety nie działa mi metoda start() Mógłby ktoś powiedzieć dlaczego? Mój program wygląda następująco, a kompilator nie zgłasza błędów

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;

public class stoper extends JFrame implements Runnable
{ 
  
int x1 = 50; 
int y1 = 50; 
Thread stoper = null;
Thread s1 =null;
int Licznik = 0; 
JLabel labelka;
JButton button;

public void run() {
	while(true){
		try{
		Thread.sleep(1000);	
		}
		catch (InterruptedException e){} 	    
		Licznik++;
	   labelka.setText("Licznik :"+Licznik); 
	  }   
	}

stoper(){
	
	ActionListener al = new ActionListener() {
		
		public void actionPerformed(ActionEvent e) {
			if(s1 == null){
			Thread s1 = new Thread();
			s1.start();	
			}
		}
	};
	
	setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	setSize(200,200);
	labelka = new JLabel("bvcxsdfghjgfds");
	button = new JButton("abc");
	button.addActionListener(al);
	setVisible(true);
	add(labelka,BorderLayout.NORTH);
	add(button,BorderLayout.SOUTH);
}
  

public static void main(String args[]){
	SwingUtilities.invokeLater(new Runnable() {
		public void run() {
			new stoper();
		}
	});
}

}
0

Bo jak robisz wątek przez implementowanie runnable to musisz zrobić coś takiego:

new Thread(runnable);

Gdzie runnable to jest obiekt implementujący taki interfejs. W kodzie który pokazałeś tworzysz sobie tak jakby pusty Thread i go startujesz.

0

zmieniłem
Thread s1 = new Thread();
na
Thread s1 = new Thread(runnable);
ale kompilator wyrzucił mi, że runnable can't be resolved to variable
Coś pomieszałem?

0
Shalom napisał(a)

Gdzie runnable to jest obiekt implementujący taki interfejs.

To znaczy, że sam musisz napisać klasę implementującą interfejs Runnable i w konstruktorze przekazać obiekt tej klasy.

0

@Noname ty w ogóle rozumiesz co piszesz? Powinieneś tam mieć Thread(this) w twoim przypadku...

0

Tak, ale nie przyjmuje mi tego w ramach ActionListenera. Jestem ciekawy, czy wtedy this odwołuje się do obiektu typu ActionPerformed. Rozwiązałem problem, robiąc tak samo, jak ty tu radzisz, jednak utworzenie wątku zrobiłem bezpośrednio w konstrukorze, a metodę start() w obsłudze zdarzeń przycisku i podziałało. Stąd miałem początkowe problemy z rozwiązaniem problemu. Niemniej, wielkie dzięki za pomoc, naprowadziłeś mnie :0

0

Raczej do obiektu typu ActionListener.

1

@Noname nie ma to jak robić naokoło :P Da sie to zrobić tak jak chciałeś:

  • piszesz w klasie metodę getSelf() z której zwracasz this
  • z poziomu action listenera możesz tą metodę wywołać
  • Thread(getSelf()); i voila :)
0

Bardzo ciekawy pomysł Shalom, nie znałem go. Dzięki za odpowiedzi. Tylko w tym wypadku tworzyć nowy wątek przy każdym naciśnięciu buttona nie ma sensu, bo licznik zaczyna inkrementować coraz więcej wątków. Skutkiem tego jest przyśpieszenie czasu, a na to się fizycy nie chcą zgodzić.
Nadziałem się jednak na inny problem. Chciałem, idąc za ciosem, tym samym klawiszem resetować licznik.
wrzuciłem więc pod Actionperformed ifa, który, jak mniemam sprawdza czy wątek działa, ale potem wszystkie moje próby zatrzymania wątku kończyły się java.lang.IllegalThreadStateException
:(

Jak zrobić taki reset, tzn skończyć wątek a potem ponownie go uruchomić, żeby przy okazji nie wyrzucić takiego wyjątku?
ActionListener al = new ActionListener() {

	<code>public void actionPerformed(ActionEvent e) {
		if(s1.isAlive()==true)
			//???
		s1.start();
	}
0

Nie wiem czy jesteś świadomy, że w tym kodzie

        ActionListener al = new ActionListener() {
 
                public void actionPerformed(ActionEvent e) {
                        if(s1 == null){
                        Thread s1 = new Thread(getSelf());
                        s1.start();        
                        }
                }
        };

występują dwie różne zmienne s1. W porównaniu s1==null występuje pole z klasy Stoper, które jest cały czas nullem, w konstruktorze Thread s1 = new Thread(getSelf());
występuje zmienna lokalna.
Zmień kod na taki:

        ActionListener al = new ActionListener() {
 
                public void actionPerformed(ActionEvent e) {
                        if(s1 == null){
                        s1 = new Thread(getSelf());
                        s1.start();        
                        }
                }
        };
0

@Noname:

  1. Zrób w twoim Runnable pole boolean continueExecuting
  2. Warunek w pętli run zmień na while(continueExecuting)
  3. Zrób metodę stopThread w którym ustawisz continueExecuting na false (co przerwie pętlę i zakończy wątek)
0

I zrob to pole volatile aby miec pewnosc. Zbyt wiele osob o tym zapomina.

0

Zrobiłem wcześniej tak jak shalom, ale nie działało, bo w if dałem =true zamiast ==true :( Lamerski, a głupi do wykrycia błąd.
Ale to działa, jak inny przycisk resetuje, nie jak resetuję tym samym przyciskiem, zobaczcie. W tym przypadku zmiana wartości boolean jest jakby niezauważona przez wątek. Dlaczego i jak to naprawić?
P.S.
wartości funkcji i zmiennej zmieniłem na takie, jakie podał shalom, żeby się łatwiej rozczytać. Funkcji volatile nie znam, nie wiem jak jej użyć.

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;

public class stoper2 extends JFrame implements Runnable
{
boolean continueExecuting=true;
int i=0;
Thread s1 =null;
int licznik = 0; 
JLabel labelka;
JButton button, button1;

stoper2 getSelf(){
	return this;
}

void stopThread(){
	continueExecuting=false;
	System.out.println("Wartosc continueExecuting :"+continueExecuting);
}

void startThread(){
	continueExecuting=true;
	System.out.println("Wartosc continueExecuting :"+continueExecuting);
}

public void run() {
	while(continueExecuting==true){
		try{
		Thread.sleep(1000);	
		}
		catch (InterruptedException e){} 	    
		licznik++;
	   labelka.setText("licznik :"+licznik); 
	   System.out.println("Wartosc continueExecuting :"+continueExecuting);
	  }
		startThread();
	}

stoper2(){
	
	ActionListener al = new ActionListener() {
		
		public void actionPerformed(ActionEvent e) {
			if(i!=0)
				stopThread();
			else
				i++;
			s1 = new Thread(getSelf());
			s1.start();
		}
	};
	
ActionListener bl = new ActionListener() {
		
		public void actionPerformed(ActionEvent e) {
			stopThread();
		}
	};
	System.out.println("Wartosc continueExecuting :"+continueExecuting);
	setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	setSize(200,200);
	labelka = new JLabel("Stoper");
	button = new JButton("abc");
	button1 = new JButton("abc-");
	button.addActionListener(al);
	button1.addActionListener(bl);
	setVisible(true);
	add(labelka,BorderLayout.NORTH);
	add(button,BorderLayout.EAST);
	add(button1,BorderLayout.WEST);
	//s1 = new Thread(getSelf());
	//analogicznie, bez użycia metody getSelf(), bo w actionlistenerze 'this' nie zadziała s1 = new Thread(this);
}
 
public static void main(String args[]){
	SwingUtilities.invokeLater(new Runnable() {
		public void run() {
			new stoper2();
		}
	});
}


}
0
  1. Ale to jest w ogóle źle napisane. Wywal metodę startExecuting i jej wywołania, bo jest bez sensu i nic nie robi
                        if(i!=0)
                                stopThread();
                        else
                                i++;
                        s1 = new Thread(getSelf());
                        s1.start();

Czyli:

  • jeśli i!=0 to zatrzymaj wątek
  • jeśli nie to zrób i++
  • niezależnie od warunków stwórz nowy wątek (z jego własną zmienną boolowską która jest na true) i go wystartuj
    O to chodziło?
0

Tak, o to chodziło. Bo przy pierwszym naciśnięciu buttona nie ma jeszcze wątku, który można zatrzymać. Czyli po pierwszym if'ie będzie on już zawsze spełniony (tak, mogłem użyć booleana, efekt jest ten sam). Działanie ma być takie, że przy pierwszym naciśnięciu licznik startuje, przy drugim i każdym kolejnym się resetuje (czyli kończę ostatni wątek i rozpoczynam nowy). Problem polega na tym, że pętla while nie odpowiada mi na zmianę ContinueExecuting na false.

stoper(){
...
	ActionListener al = new ActionListener() {
		
		public void actionPerformed(ActionEvent e) {
			if(i!=0)
				stopThread();
			else
				i++;
			s1 = new Thread(getSelf());
			s1.start();
		}
	};
...
}
public void run() {
	System.out.println("Wartosc continueExecuting/// :"+continueExecuting);
	while(continueExecuting==true){
		try{
		Thread.sleep(1000);	
		}
		catch (InterruptedException e){} 	    
		licznik++;
	   labelka.setText("licznik :"+licznik); 
	   System.out.println("Wartosc continueExecuting ***:"+continueExecuting);
	   }
	continueExecuting=true;
}
}
Wartosc continueExecuting :true (na początku programu)
                                                   (naciśnięcie buttona po raz I)
Wartosc continueExecuting/// :true 
Wartosc continueExecuting ***:true 
Wartosc continueExecuting ***:true
Wartosc continueExecuting ***:true
                                                  (naciśnięcie buttona po raz II)
Wartosc continueExecuting :false
Wartosc continueExecuting/// :false
Wartosc continueExecuting ***:true (skąd ta zmiana wartości???)
Wartosc continueExecuting ***:true
0
public void run() {
	System.out.println("Wartosc continueExecuting/// :"+continueExecuting);
	while(continueExecuting==true){
		try{
		Thread.sleep(1000);	
		}
		catch (InterruptedException e){} 	    
		licznik++;
	   labelka.setText("licznik :"+licznik); 
	   System.out.println("Wartosc continueExecuting ***:"+continueExecuting);
	   }
	continueExecuting=true;
} 

Poprawka, bo w poprzednim kodzie linijka " continueExecuting=true; " należała do pętli. Tak na prawdę nie należy a wrtość się zmienia, tak, jakby należała. Nie wiem dlaczego.

0
public void run() {
        System.out.println("Wartosc continueExecuting/// :"+continueExecuting);
        while(continueExecuting==true){
                try{
                Thread.sleep(1000);
                //watek sobie śpi, a w tym czasie rusza drugi wątek i zmienia coninueExcecuting na true        
                }
                catch (InterruptedException e){}             
                licznik++;
           labelka.setText("licznik :"+licznik); 
           System.out.println("Wartosc continueExecuting ***:"+continueExecuting);
           }
        continueExecuting=true;
} 
0

rozwiązanie problemu? Byłbym wdzięczny.

0

@Noname ty sobie robisz jaja? Kazałem ci usunąć metodę startThread, ale całkiem! A nie że ty sobie tą bzdurną linijkę z niej przepisałeś. PO CO ustawiasz na końcu continueExecuting na true?
Poza tym @bo napisał ci w czym problem:
może się tak zdarzyć ze zrobisz stopThread a on wcale się nie zastopuje bo akurat śpi
Nawet gdyby tak nie było, to cały czas jest tam race condition.

Jak to rozwiązać? Ten wątek powinien być customowym wątkiem. Lekcja na dziś:

  • zasada jednej odpowiedzialności
    NIE robimy tak że klasa robi 10 rzeczy na raz. Klasa odpowiedzialna za GUI to jedna klasa. Klasa odpowiedzialna za wątek (Thread/Runnable) to druga klasa. W ten sposób continueExecuting ma być polem klasy odpowiedzialnej tylko za wątek. W ten sposób to pole będzie powiązane z konkretnym wątkiem a nie z GUI.
0

No ale gdzieś muszę z powrotem zmienić wartość na true, bo kolejne przyciśnięcie klawisza tylko skończy wątek a nie uruchomi kolejnego(tzn, uruchomi, ale wszystko w pętli się nie wykona).

0

Nie. Jak zrobisz tak jak napisalem - wydzielając osobną klasę dla wątku, to nie będzie trzeba takich rzeczy robić.

0

A jak odmierzać czas (bo chyba tam gdzieś sekundy sobie zliczasz) to lepiej to zdaje się zrobić na Timerze. Możesz sobie go zatrzymywać wznawiać resetować itd itp :)

http://docs.oracle.com/javase/1.5.0/docs/api/javax/swing/Timer.html

a przykład prosto z powyższej po modach strony (odmierzanie sekundy)

 public Timer timer;
....
  int delay = 1000; //milliseconds
  ActionListener taskPerformer = new ActionListener() {
      public void actionPerformed(ActionEvent evt) {
          Licznik++;
      }
  };
  timer=new Timer(delay, taskPerformer);
timer.start();

a actionlistener pod klawisz wyglądałby tak

      public void actionPerformed(ActionEvent evt) {
         Licznik=0;
        timer.restart();
      }
0

Wiem, że to wbrew forum. Ale czy mógłbym na kodzie zobaczyć poprawnie działający program? Bo nie wiem np. dlaczego coś co ma działać w dwóch klasach nie sprawdza się w jednej (pomimo, że jest to zgodne z konwencją programowania obiektowego). Potrzebuję po prostu przykładu takiego stopera, który działa w 100%, żeby się na nim poznać i wzorować. Długo już siedzę nad tym programem i już po prostu nie wiem, co jeszcze robię źle. Potrzeba mi dobrego przykładu, jak y tego...

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;

public class stoper2 extends JFrame implements Runnable
{
boolean continueExecuting=true;
int i=0;
Thread s1 =null;
int licznik = 0; 
JLabel labelka;
JButton button, button1;

stoper2 getSelf(){
	return this;
}

void stopThread(){
	continueExecuting=false;
	System.out.println("Wartosc continueExecuting... :"+continueExecuting);
}

public void run() {
	System.out.println("Wartosc continueExecuting/// :"+continueExecuting);
	while(continueExecuting==true){
		try{
		Thread.sleep(1000);	
		}
		catch (InterruptedException e){} 	    
		licznik++;
	   labelka.setText("licznik :"+licznik); 
	   System.out.println("Wartosc continueExecuting ***:"+continueExecuting);
	   }
	}

stoper2(){
	
	ActionListener al = new ActionListener() {
		
		public void actionPerformed(ActionEvent e) {
			if(i!=0)
				stopThread();
			else
				i++;
			s1 = new Thread(getSelf());
			s1.start();
		}
		
	};
	
ActionListener bl = new ActionListener() {
		
		public void actionPerformed(ActionEvent e) {
			stopThread();
		}
	};
	System.out.println("Wartosc continueExecuting :"+continueExecuting);
	setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	setSize(200,200);
	labelka = new JLabel("Stoper");
	button = new JButton("abc");
	button1 = new JButton("abc-");
	button.addActionListener(al);
	button1.addActionListener(bl);
	setVisible(true);
	add(labelka,BorderLayout.NORTH);
	add(button,BorderLayout.EAST);
	add(button1,BorderLayout.WEST);
	//s1 = new Thread(getSelf());
	//analogicznie, bez użycia metody getSelf(), bo w actionlistenerze 'this' nie zadziała s1 = new Thread(this);
}
 
public static void main(String args[]){
	SwingUtilities.invokeLater(new Runnable() {
		public void run() {
			new stoper2();
		}
	});
}


}

..zrobić działający program, lub program który już tak działa.

0

Takie coś zrób na stoperze, tak jak Ci podałem w poprzednim poście.
Nie działa Ci to teraz na pewno, bo

  1. startujesz miliard wątków kiedy wciskasz przycisk za to odpowiedzialny ( jeżeli istnieje wątek liczący to go najpierw zatrzymaj, dopiero startuj nowy)
  2. nigdzie nie ustawiasz na true warunku pętli P

Idąc dalej - tam masz coś że Ci this nie działa. Oczywiście, że this Ci nie zadziała, bo this odnosi się do kontekstu ( w kodzie) w którym jest umieszczony. Wiesz co to klasy anonimowe ?? To takie klasy których sam sobie nie tworzysz w osobnym pliku, tylko klepiesz je bezczelnie w kodzie, a potem w klamrach albo implementujesz im abstrakcyjne metody (interfejsy) albo nadpisujesz pewne metody(@Override). Przykład ? a no właśnie

        ActionListener bl = new ActionListener() {
            public void actionPerformed(ActionEvent e) {
            }
        };

wygląda znajomo?
bl nie jest obiektem klasy ActionListener, bo po primo ActionListener jest interfejsem (a interfejs nie ma swoich instancji tylko klasy), secundo tworzysz tutaj właśnie klasę anonimową którą kompilator nazwie Ci mniej więcej tak- Nazwa_klasy_zewnętrznej_$n (chyba z _) gdzie klasa zewnetrzna to jest ta wewnątrz której wkeisz ten kod a n to jest numer która to z kolei anonimowa klasa jest w twoim kodzie. Także bl jest obiektem klasy której nazwy nie znasz (bo to klasa anonimowa) implementującego interfejs ActionListener. Także jeżeli wewnątrz actionPerdorma dasz this, to this będzie właśnie wskazywał na tą klasę anonimową a nie na Twoją stoper2.
Do stoper2 zamiast getself można się jeszcze dobrać wstawiając takie coś

Stoper.this // i to będzie referencja na obiekt typu Stoper w którym to wywołanie się znalazło

na temat tego jestem pewno ze Shalom się wypowie odpowiednio

A co do Twojego rozwiązania, to masz tutaj zrobione po twojemu z modami (chociaż bez sensu robota, od tego jest Timer;]):

 package testowa;

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;

public class Stoper extends JFrame implements Runnable {

    volatile boolean continueExecuting = true;
    int i = 0;
    Thread s1 = null;
    int licznik = 0;
    JLabel labelka;
    JButton button, button1;

    public Stoper getSelf() {
        return this;
    }

    void stopThread() {
        continueExecuting = false;
        System.out.println("Wartosc continueExecuting... :" + continueExecuting);
    }

    public void run() {
        System.out.println("Wartosc continueExecuting/// :" + continueExecuting);
        while (true) {
            try {
                for (int i = 0; i < 100 && continueExecuting == true; i++) {
                    Thread.sleep(10);
                }
            } catch (InterruptedException e) {
            }
            if(continueExecuting==true)
                licznik++;
            else break;
            labelka.setText("licznik :" + licznik);
            System.out.println("Wartosc continueExecuting ***:" + continueExecuting);
        }
    }

    public Stoper() {

        ActionListener al = new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                stopThread();
                if (s1 != null) {
                    try {
                        s1.join();
                    } catch (InterruptedException ex) {
                        Logger.getLogger(Stoper.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
                continueExecuting = true;
                licznik = 0;
                labelka.setText("licznik :" + licznik);
                s1 = new Thread(getSelf());
// lub
// s1=new Thread(Stoper.this);
                s1.start();
            }
        };

        ActionListener bl = new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                stopThread();
            }
        };
        System.out.println("Wartosc continueExecuting :" + continueExecuting);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(200, 200);
        labelka = new JLabel("Stoper");
        button = new JButton("Start/Restart");
        button1 = new JButton("Stop");
        button.addActionListener(al);
        button1.addActionListener(bl);
        setVisible(true);
        add(labelka, BorderLayout.NORTH);
        add(button, BorderLayout.EAST);
        add(button1, BorderLayout.WEST);
        //s1 = new Thread(getSelf());
        //analogicznie, bez użycia metody getSelf(), bo w actionlistenerze 'this' nie zadziała s1 = new Thread(this);
    }

    public static void main(String args[]) {
        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                new Stoper();
            }
        });
    }
}
0

Wielkie dzięki. Teraz będę wiedział, jak to się powinno pisać. Zresztą zrobiłem tu tyle błędów, że wiem, jak tego nie należy pisać bardziej :) Jeszcze raz dzięki za poświęcony mi czas (to moje początki, więc wiem, że tłumaczenie mi błędów mogło być irytujące ) ale to dzięki wam zacząłem to ogarniać. Jak to dobrze że programiści mogą liczyć na takie zaplecze, jak Wy. Wesołego Nowego Roku!

0

Jejku jej, ale ten kod kolegi @Antoniossss wcale niekoniecznie zadziała :P Tzn jedyne co tam się zmieniło to mały hack na wykonywanie wielokrotnie małych sleepów zamiast jednego dużego. Ale to nadal jest tragedia! :) Zasada jednej odpowiedzialności!

package testowa;

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;

public class Stoper extends JFrame {
    private static final long serialVersionUID = 1L;
    private WatekOdliczajacy s = null;
    private int licznik = 0;
    private final JLabel labelka;
    private final JButton button, button1;

    void stopThread() {
        if (s != null) {
            s.stopThread();
        }
    }

    public Stoper() {
        ActionListener al = new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                stopThread();
                if (s != null) {
                    try {
                        s.join();
                    } catch (InterruptedException ex) {
                        Logger.getLogger(Stoper.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
                licznik = 0;
                s = new WatekOdliczajacy(getSelf());
                s.start();
            }
        };

        ActionListener bl = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                stopThread();
            }
        };

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(200, 200);
        labelka = new JLabel("Stoper");
        button = new JButton("Start/Restart");
        button1 = new JButton("Stop");
        button.addActionListener(al);
        button1.addActionListener(bl);
        setVisible(true);
        add(labelka, BorderLayout.NORTH);
        add(button, BorderLayout.EAST);
        add(button1, BorderLayout.WEST);
    }

    protected Stoper getSelf() {
        return this;
    }

    public static void main(String args[]) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new Stoper();
            }
        });
    }

    public void odlicz() {
        licznik++;
        labelka.setText("licznik :" + licznik);
    }
}

class WatekOdliczajacy extends Thread {
    private volatile boolean continueExecuting = true;
    private final Stoper stoper;

    public WatekOdliczajacy(Stoper s) {
        stoper = s;
    }

    public void stopThread() {
        continueExecuting = false;
    }

    @Override
    public void run() {
        while (continueExecuting) {
            try {
                stoper.odlicz();
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
    }

}
0

@Shalom
E tam nie mówiłem, że ładnie to jest napisane, ale miało być zmodowane to co autor podał :) dzięki i tak, że nie zrównałeś z ziemią :) u mnie działało w każdym razie :P

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