Okienko zmienia pozycję po zmianie rozdzielczości

0

Witam serdecznie. Posiadam gotową aplikację która jest cały czas wyśrodkowana bez możliwości zmiany jej położenia. Niestety problem pojawia się w momencie kiedy np. odpalę jakąś grę w innej rozdzielczości niż monitora to okienko z ową aplikacją zostaje wywalone w prawy dolny róg ekranu. Czy istnieje jakiś trick pozwalający zapobiec temu? Czy jest możliwość aby aplikacja nawet przy zmianie rozdziałki zostawała na wyśrodkowanej pozycji? Powiedzmy że się nie znam na tym dokładnie, aczkolwiek myślę że w jakimś minimalnym stopniu coś rozumiem. Korzystam z projektu w Lazarusie 1.6.4.

1
Eazy napisał(a):

Posiadam gotową aplikację która jest cały czas wyśrodkowana bez możliwości zmiany jej położenia.

Bez możliwości zmiany położenia okna za pomocą standardowych mechanizmów. Nie dotyczy to zmiany rozmiaru i położenia okna z poziomu kodu.

Niestety problem pojawia się w momencie kiedy np. odpalę jakąś grę w innej rozdzielczości niż monitora to okienko z ową aplikacją zostaje wywalone w prawy dolny róg ekranu.

Jeśli odpalisz grę w trybie graficznym to system zrobi z oknami to, co mu się podoba.

Czy istnieje jakiś trick pozwalający zapobiec temu?

Jeśli coś nie działa jak należy i pomimo używania wymienionej opcji formularz pokazuje się gdzieś indziej niż na środku ekranu, należy napisać sobie prostą metodę, która obliczy pozycję okna, np. na podstawie danych z obiektu Screen i użyć jej np. w zdarzeniu OnShow okna.

Czy jest możliwość aby aplikacja nawet przy zmianie rozdziałki zostawała na wyśrodkowanej pozycji?

Jeżeli formularz sam nie reaguje na zmianę rozdziałki (IMO nie powinien reagować) to – tak jak już pisałem Ci wcześniej – należy dodać obsługę komunikatu, wysyłanego do aplikacji po zmianie rozdzielczości.

Tym komunikatem jest WM_DISPLAYCHANGE, a tutaj masz przykład złapania i obsługi tego komunikatu przez formularz. Co prawda przykład jest dla Delphi, ale nie powinno być problemu również pod Lazarusem.

0

I jak mniej więcej należałoby to wpisać do kodu?

1

Spróbuj tak:

uses
  Windows, {..};

type
  TForm1 = class(TForm)
  protected
    procedure WMDisplayChange(var AMessage: TWMDisplayChange); message WM_DISPLAYCHANGE;
  end;

{..}

procedure TForm1.WMDisplayChange(var AMessage: TWMDisplayChange);
begin
  Self.Left := (AMessage.Width  - Self.Width)  div 2;
  Self.Top  := (AMessage.Height - Self.Height) div 2;
end;
0

Dodałem, przekompilowało się bez problemu natomiast dalej okienko się przesuwa, gdy nastąpi jakaś zmiana rozdzielczości.

1

Nie wiem co jest tego powodem – nie działa ani w standardowy sposób (dodając metodę obsługi komunikatu), ani w WndProc. Widać ten komunikat w ogóle nie dociera do okna.


Edit: póki nie da się normalnie przechwycić i obsłużyć tego komunikatu, pozostaje paskudne obejście tego problemu, czyli np. timer sprawdzający rozdzielczość ekranu, na którym wyświetlane jest okno aplikacji. Niech ten timer np. co 100ms sprawdza rozdzielczość i ją zapamiętuje. Jeśli rozdziałka ekranu nie będzie pasować do tej zapamiętanej, niech zmieni położenie okna i przechowa nowe wymiary – i tak w kółko.

Jeśli chcesz się dowiedzieć dlaczego nie da się tego komunikatu obsłużyć w normalny sposób, zadaj pytanie na forum Lazarusa – może ktoś będzie w stanie stwierdzić co jest przyczyną (być może gdzieś jest bug w LCL).

0

No to pozostaje na sztywno wpisać współrzędne do kodu proponuje współrzędne lewego górnego rogu czyli 0,0. Po zmianie rozdzielczości okienko powinno wylądować na środku :)

0

Na jakiej podstawie zakładacie, że zdarzenie OnShow zostanie wygenerowane, gdy system zmieni rozdzielczość? Bo coś mi się wydaje, że aplikacj "nie wie" że zmieniła sie rozdzielczość, a system sam rozmieszcza okna na ekranie jak chce, a zalezy to od wielu rzeczy, także od tego, w jaki sposób zmieniana jest rozdzielczość przez grę.

0
Krzywy Programista napisał(a):

Na jakiej podstawie zakładacie, że zdarzenie OnShow zostanie wygenerowane, gdy system zmieni rozdzielczość?

A na jakiej podstawie zakładasz, że ktoś z nas zakłada, że OnShow zostanie wtedy wygenerowane?

Pisałem o tym zdarzeniu, że można w nim ustalić położenie okna, jeśli samo nie pokazuje się na środku. Absolutnie nie ma to zastosowania w momencie, gdy okno już jest widoczne.

Bo coś mi się wydaje, że aplikacj "nie wie" że zmieniła sie rozdzielczość […]

Formularz nie wyłapuje komunikatu WM_DISPLAYCHANGE (chwilowo nie wiadomo dlaczego) nawet przy zwykłej (manualnej) zmianie rozdzielczości.

[…] a system sam rozmieszcza okna na ekranie jak chce, a zalezy to od wielu rzeczy, także od tego, w jaki sposób zmieniana jest rozdzielczość przez grę.

Jeśli użytkownik uruchamia grę pełnoekranową w trybie graficznym, to i tak okna zwykłych programów nie będą widoczne, więc nie ma sensu kombinować.

2

Nie wiem dlaczego nie wyłapuje, ale to może być trop:

Message-Only Windows

A* message-only window* enables you to send and receive messages. It is not visible, has no z-order, cannot be enumerated, and does not receive broadcast messages. The window simply dispatches messages.

https://msdn.microsoft.com/en-us/library/windows/desktop/ms632599%28v=vs.85%29.aspx#message_only

Jeśli Lazarus komunikaty zadeklarowane słowem kluczowym message obsługuje poprzez takowe message-only window, to podkreślony fragment może być wytłumaczeniem.

Trzeba byłoby spróbować przez override procedury WndProc złapać ten komunikat.

Tylko pytanie co właściwie chcesz osiągnąć - i po co.
Jeśli odpalasz grę pełnoekranową, to gra po wyjściu przywraca oryginalną rozdzielczość, i twoje okno powinno być tam gdzie było.
Chcesz powiedzieć że twoje okno jest zawsze na wierzchu, na środku ekranu, nawet jeśli jest uruchomiona pełnoekranowa gra? ;-)
Czy może że po powrocie z gry rozdzielczość wraca ale okno pozostaje przesunięte. To byłoby dziwne. To by oznaczało, że robisz coś dziwnego z pozycjonowaniem tego okna, skoro mechanizm zmiany rozdzielczości coś ci psuje.
Bo spójrz wokoło: 99% aplikacji nic nie robi z rozdzielczością, a po wyjściu z gry wszystko jest jak było na swoim miejscu. I nie trzeba nic robić.

0

Czy może że po powrocie z gry rozdzielczość wraca ale okno pozostaje przesunięte.

Dokładnie tak się dzieje. Co więcej okienka "windowsowe" tzn. na przykład foldery(otworzone rzecz jasna) robią to samo, zaś inna aplikacja pisana prawdopodobnie w WinAPI normalnie sobie z tym radzi i cały czas jest na środku nawet po wyłączeniu gry i przywróceniu rozdzielczości.

1
Azarien napisał(a):

Trzeba byłoby spróbować przez override procedury WndProc złapać ten komunikat.

Próbowałem, ale to nic nie daje, bo ten komunikat w ogóle nie dociera do głównego okna.

Tylko pytanie co właściwie chcesz osiągnąć - i po co.

Ja tego też będę potrzebował, w niedalekiej przyszłości.

Mam aplikację, która zawsze działa na full screen, przykrywając pasek zadań i zwykłe okna. Główne okno aplikacji musi reagować na zmianę rozdzielczości, aby zawsze zajmowało cały ekran. Inna sprawa – program testowałem na tablecie z Win10, który obraca pulpit o 90° po obróceniu ekranu. Wtedy program też musi dostosowywać rozmiar okna do nowej rozdzielczości.


Eazy napisał(a):

[…] zaś inna aplikacja pisana prawdopodobnie w WinAPI normalnie sobie z tym radzi i cały czas jest na środku nawet po wyłączeniu gry i przywróceniu rozdzielczości.

Tu nie chodzi o to, że tamten program został napisany bez użycia biblioteki komponentów. Po prostu jego autor, widząc że okno zmienia położenie po zamknięciu gry, napisał kod, który aktualizuje położenie okna po zakończeniu działania procesu.

Ty też możesz to zrobić – w swoim programie i tak czekasz na zakończenie działania procesu, więc linijkę dalej dodaj kod, który obliczy współrzędne i przesunie okno w zadane miejsce.

0

"aktualizuje położenie okna po zakończeniu działania procesu" Właśnie że nie. Program uruchamiam osobno a grę osobno więc w takim przypadku nie ma mowy o czekaniu na zakończenie procesu. Stosując ten zabieg aplikacja z WinAPI trzyma się na środku cały czas zaś ta z Lazarusa się przemieszcza.

1

W takim razie zrób timer, o którym pisałem wcześniej – też będzie działać. Przynajmniej na razie, póki nie wymyśli się sposobu na przechwycenie komunikatu.

2

Nie wiem czemui ten komunikat nie działa w Lazarusie ale jak bym miał używać timera to chyba lepiej kombinować z przechwyceniem WM_WINDOWPOSCHANGED lub WM_WINDOWPOSCHANGING ale raczej ten pierwszy.

2

Jednak Lazarus też może obsłużyć WM_DISPLAYCHANGE wprawdzie w dość nietypowy sposób ale jednak:

var
  Form1: TForm1;
  hwnd: THandle;

implementation

{$R *.lfm}

{ TForm1 }

uses
  lclintf;

procedure TForm1.MyWndProc(var Msg: TLMessage);
begin
  if Msg.msg = WM_DISPLAYCHANGE then
  begin
    //tu kod
  end;
  Msg.result:= DefWindowProc(hwnd, Msg.msg, Msg.wParam, Msg.lParam);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  hwnd:= lclintf.AllocateHWnd(@MyWndProc);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  lclintf.DeallocateHWnd(hwnd);
end; 
0
uses
  Windows, Registry, SysUtils, Forms, ExtCtrls, AutorunButton, Graphics, LCLIntf, ShellAPI, LMessages;

var
  MainForm: TMainForm;   

procedure TMainForm.FormCreate(ASender: TObject);
begin
  FProcessing := False;
  LoadBitmapsToControls();

  PrepareAutorunData();
  PrepareInstallationData();
  PrepareButtons();
end;    

Tutaj urywek mojego kodu. Czy mógłby mi ktoś pomóc zaaplikować rozwiązanie @kAzek do tego kodu? Ja oczywiście otrzymuje warningi i nie wiem co jest źle. Na czystym kodzie sprawdzałem i faktycznie działa.

@Edit: Chyba jednak nie działa.

2

Jak nie działa chodzi o centrowanie okna po zmianie rodzielki tak?
No to co za problem deklarujesz sobie klasę:

type
  TFormPosCenterFix = class(TComponent)
  private
    fForm: TForm;
    hMsgHandlerWnd: Cardinal;
    procedure MsgHandler(var Msg: TMessage);
  published
    property Form: TForm read fForm write fForm;
  public
    constructor Create(AForm: TForm); overload;
    destructor Destroy; override;
  end; 

i implementacja:


uses lclintf;

procedure TFormPosCenterFix.MsgHandler(var Msg: TMessage);
begin
  if Msg.Msg = WM_DISPLAYCHANGE then
  begin
    fForm.Left:= (Msg.LParamLo - fForm.Width) div 2;
    fForm.Top := (Msg.LParamHi - fForm.Height) div 2;
  end;
  Msg.result:= DefWindowProc(hMsgHandlerWnd, Msg.Msg, Msg.WParam, Msg.LParam);
end;

constructor TFormPosCenterFix.Create(AForm: TForm);
begin
  inherited Create(AForm);
  fForm:= AForm;
  hMsgHandlerWnd:= lclintf.AllocateHWND(@MsgHandler);
end;

destructor TFormPosCenterFix.Destroy;
begin
  lclintf.DeallocateHWND(hMsgHandlerWnd);
  inherited Destroy;
end; 

i teraz gdziekolwiek w konstruktorze Form tworzysz sobie:

procedure TForm1.FormCreate(Sender: TObject);
begin
  TFormPosCenterFix.Create(Self);
end;   

i ma działać.

2

@Eazy: odpowiadając na peemkę – dodaj moduł Classes do listy uses w module okna, skopiuj deklarację klasy TFormPosCenterFix i wklej ją ponad deklaracją klasy TMainForm. Następnie skopiuj definicje metod tej klasy i wklej je do sekcji implementation w module okna (w dowolne miejsce, byle w tej sekcji).

Na koniec dodaj poniższą linijkę do konstruktora TMainForm:

TFormPosCenterFix.Create(Self);
2

Muszę dodać kilka ważnych informacji, jeśli chodzi o sposób uznany w tym wątki za rozwiązanie problemu.

Kończę właśnie implementować właściwy mechanizm reagujący na zmianę rozdzielczości ekranu. Póki co testy przeprowadziłem pod WinXP – pod nowszymi sprawdzę w najbliższym czasie. Mowa oczywiście o Lazarusie, w wersji 1.6.4.


1. Kolejność instrukcji

Rejestrowanie własnej metody obsługi przychodzących komunikatów musi być zrealizowane po wywołaniu metody Application.Initialize. Jeśli zrobi się to wcześniej, to metoda ta nie będzie otrzymywać żadnych komunikatów i stanie się bezwartościowa. Czyli aby sposób działał poprawnie, poniższa kolejność linijek kodu musi zostać zachowana:

Application.Initialize();

{..}

SomeHandle := LCLIntf.AllocateHWnd(@HandlerMethod);

2. Wartości komunikatu

Dane dostarczane wraz z komunikatem zawierają nową (czyli w momencie przetwarzania komunikatu: aktualną) szerokość i wysokość ekranu, na podstawie nowych ustawień. Jak informuje nas dokumentacja:

lParam

The low-order word specifies the horizontal resolution of the screen. The high-order word specifies the vertical resolution of the screen.

Problem w tym, że dane te dotyczą głównego ekranu, a nie tego, którego rozdzielczość została zmieniona. Jeśli okna programu wyświetlone są na ekranie pobocznym i tego ekranu rozdzielczość zostanie zmieniona, komunikat i tak dostarczy szerokość i wysokość okna głównego. Tak więc na tych danych nie można bazować, jeśli program może być wyświetlany na dowolnym ekranie.

Na pewno lepszym rozwiązaniem będzie skorzystanie z metody Screen.MonitorFromWindow w celu pobrania obiektu z danymi monitora, a następnie pozyskanie właściwej rozdzielczości za pomocą właściwości TMonitor.Width i TMonitor.Height.

Jeszcze tego nie sprawdzałem i nie wiem jak to zadziała, gdy program wyświetlany jest na monitorze drugorzędnym i monitor ten zostanie wyłączony w systemowych ustawieniach ekranu.

3. Niespodziewany wyjątek

W aplikacji testowej sprawdziłem jak zachowa się ten mechanizm, gdy widoczne jest okno główne i dialogowe. Tu wszystko gra, jednak w pewnym momencie, po którejś z kolei zmianie rozdzielczości i aktualizacji placementu okien, dostałem wyjątek SIGFPE. Niestety problemu póki co nie zlokalizowałem.

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