• 0
ddr 2

[Android] обмен сообщениями между потоками и формой

Вопросы

Привет

Под WIN32/64:

 - сообщение из формы в поток отправлял PostThreadMessage(myThread.ThreadID, WM_user+1, nativeuint(@data1),nativeuint(@data2));

- сообщение из потока в форму SendMessage(FormHandle,WM_user+2,nativeuint(@data1),nativeuint(@data2)); 

В Android такого нет. Подскажите, как организовать обмен сообщениями между формой и потоками в Android?

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

9 ответов на этот вопрос

  • 1

Поищите на этом форуме или в гугле TThreadedQueue - универсальный метод под все платформы.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты
  • 0

Не знал про TThreadedQueue. Полезный функционал, спасибо!

Полагаю, что отсылку сообщения из потока в форму можно/нужно делать через TMessageManager. Написал небольшой тест(ниже по тексту- поток посылает сообщения, а форма принимает их и отображает ). Увы, но тест работает не корректно. При запуске под Windows, в процедуре обработке принятого сообщения выскакивает исключение,  где-то в районе label1.Text:=stringrecive; . Если в процедуре TForm2._ReciveMessage закоментировать строчки label1.Text:=stringrecive; и label2.Text:=stringrecive ,то оставшиеся добавление  строки в memo работает, но явно "не здорово" подмаргивает. При запуске под Android, изменение текста в метках работает, но memo так же дергается и моргает. Применение TThreadedQueue никак не влияет на результат и в тексте закоментировано.

Прошу помощи, в чем ошибка?

unit Unit2;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,System.Messaging,
  FMX.Controls.Presentation, FMX.ScrollBox, FMX.Memo, FMX.StdCtrls,System.Generics.Collections;

type
  tmythread=class(TThread)
  public
  constructor Create;
  protected
  packetnomber:integer;
  procedure Execute; override;
end;

  TForm2 = class(TForm)
    Memo1: TMemo;
    Label1: TLabel;
    Label2: TLabel;
    procedure FormCreate(Sender: TObject);
  private
  mythread:tmythread;
  procedure _ReciveMessage(const Sender: TObject; const M: TMessage);
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form2: TForm2;
//  qqueue: TThreadedQueue<string>;
implementation

constructor tmythread.Create;
begin
 packetnomber:=0;
 FreeOnTerminate := true;
 inherited create(false);
end;

procedure tmythread.Execute;
var strmessage:string; MessageManager: TMessageManager;_Message: TMessage;
begin
 while (not Terminated) do
 begin
 inc(packetnomber);
 strmessage:='Пакет№ '+inttostr(packetnomber)+' значение X='+inttostr(random(1000))+
 ' значение Y='+inttostr(random(1000))+' значение Z='+inttostr(random(1000));
 //qqueue.PushItem(strmessage);
 _Message:=TMessage<String>.Create(strmessage);
 MessageManager := TMessageManager.DefaultManager;
 MessageManager.SendMessage(self, _Message, true);
 sleep(random(800));
 end;
end;

{$R *.fmx}

procedure TForm2._ReciveMessage(const Sender: TObject; const M: TMessage);
var stringrecive:string;
begin
 stringrecive:=(M as TMessage<String>).Value;
 //stringrecive:=qqueue.PopItem;
 label1.Text:=stringrecive;
 label2.Text:=stringrecive;
  memo1.Lines.Add(stringrecive);memo1.GoToTextEnd;
end;

procedure TForm2.FormCreate(Sender: TObject);
var
  SubscriptionId: Integer;
  MessageManager: TMessageManager;
begin
//  qqueue := TThreadedQueue<string>.Create(5, 1000);
  memo1.StyledSettings:=[]  ;
  memo1.TextSettings.Font.Size:=8;
  label1.StyledSettings:=[];
  label1.TextSettings.Font.Size:=8;
  label2.StyledSettings:=[];
  label2.TextSettings.Font.Size:=8;

  mythread:=tmythread.Create;
  MessageManager := TMessageManager.DefaultManager;
  SubscriptionId := MessageManager.SubscribeToMessage(TMessage<String>,self._ReciveMessage);
end;
end.

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты
  • 0
5 часов назад, ddr 2 сказал:

в чем ошибка?

Поставьте бряк внутри метода

_ReciveMessage

и удивитесь - в каком потоке он вызывается.

Ну или напишите
if TThread.Current.ThreadID<>MainThreadID then // мог слегка ошибиться с наименованиями переменных - пишу по памяти, но вроде это они и есть.
  Алярма!!! (тут что-нибудь сигнализирующее что всё неправильно).

Включая режим буквоедства: смените названия на ReceiveMessage и PackedNumber. Глаз цепляется :)

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты
  • 0
В 04.02.2019 в 17:09, kami сказал:

и удивитесь - в каком потоке он вызывается.

Поставил бряк, посмотрел стек, да Вы правы, вызов идет из потока mythread. 

Я хотел реализовать "PostMessage" из потока в главный поток, которого FMX нет. Нашёл подсказку

Цитата

TThread.Queue заменяет PostMessage, если вызов идет не из главного потока.

 
Переделал.Все работает и вызов ReciveMessage происходит из главного потока(MainThread). Правильно ли я понимаю, что в подобной реализации "PostMessage"(речь не про данный код, 
а про сам подход) нужно учитывать и контролировать, что бы поток не завершился раньше, чем все "сообщения" будут обработаны основным потоком? 

Буду рад услышать, о других возможных реализация "PostMessage"

 

unit Unit2;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,System.Messaging,
  FMX.Controls.Presentation, FMX.ScrollBox, FMX.Memo, FMX.StdCtrls,System.Generics.Collections;

type
  tmythread=class(TThread)
  public
  constructor Create;
  protected
  PackedNumber:integer;
  procedure PostMessage;
  procedure Execute; override;
end;

  TForm2 = class(TForm)
    Memo1: TMemo;
    Label1: TLabel;
    Label2: TLabel;
    procedure FormCreate(Sender: TObject);
  private
  mythread:tmythread;
    { Private declarations }
  public
  procedure ReciveMessage;
    { Public declarations }
  end;

var
  Form2: TForm2;
  qqueue: TThreadedQueue<string>;
implementation

constructor tmythread.Create;
begin
 PackedNumber:=0;
 FreeOnTerminate := true;
 inherited create(false);
end;

procedure tmythread.Execute;
var strmessage:string;
begin
 while (not Terminated) do
 begin
 inc(PackedNumber);
 strmessage:='Пакет№ '+inttostr(PackedNumber)+' значение X='+inttostr(random(1000))+
 ' значение Y='+inttostr(random(1000))+' значение Z='+inttostr(random(1000));
 qqueue.PushItem(strmessage);
 Tthread.Queue(nil,PostMessage);
 sleep(random(800));
 end;
end;

{$R *.fmx}

procedure TForm2.ReciveMessage;
var stringrecive:string;
begin
 stringrecive:=qqueue.PopItem;
 label1.Text:=stringrecive;
 label2.Text:=stringrecive;
  memo1.Lines.Add(stringrecive);memo1.GoToTextEnd;
end;

procedure tmythread.PostMessage;
begin
  Form2.ReciveMessage;
end;

procedure TForm2.FormCreate(Sender: TObject);
begin
  qqueue := TThreadedQueue<string>.Create(5, 1000);
  memo1.StyledSettings:=[]  ;
  memo1.TextSettings.Font.Size:=8;
  label1.StyledSettings:=[];
  label1.TextSettings.Font.Size:=8;
  label2.StyledSettings:=[];
  label2.TextSettings.Font.Size:=8;
  mythread:=tmythread.Create;
end;
end.

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты
  • 0

На основе вашего кода накидал свой вариант.

Главная форма создает 2 очереди: FMyQueueIn - очередь из потока в форму, и FMyQueueOut - очередь из формы в поток. Тип очередей сделал вот такой: 

  TMyMessage = record
    StringMessage : String;
    PackedNumber : Integer;
    X : Integer;
    Y : Integer;
    Z : Integer;
  end;

Вдруг вам понадобятся данные в виде цифр, а не в виде строки.

Конструктор потока получает в качестве параметров эти два потока (но поменянные местами  - чтоб в контексте потока они соотвествовали своим названиям).

Так же добавил по нажатию кнопки отправку текстового  сообщения из формы в поток. Поток получает его, добавляет в начало фразу 'Из главного потока получено: '  и возвращает в форму с очередными данными.

Код и архив проекта прилагаю:

unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
  Generics.Collections,
  System.Generics.Collections, FMX.Controls.Presentation, FMX.ScrollBox,
  FMX.Memo, FMX.StdCtrls;

type

  TMyMessage = record
    StringMessage : String; //Я вынужден был добавить еще строку, иначе в Rio косяк https://quality.embarcadero.com/browse/RSP-21806
    PackedNumber : Integer;
    X : Integer;
    Y : Integer;
    Z : Integer;
  end;

  TMyQueue = TThreadedQueue<TMyMessage>;

  TMyThread = class(TThread)
  protected
    FMyQueueIn : TMyQueue;
    FMyQueueOut : TMyQueue;
    FPackedNumber : Integer;
    procedure Execute; override;
  public
    constructor Create(AMyQueueIn, AMyQueueOut : TThreadedQueue<TMyMessage>);
  end;

  TForm1 = class(TForm)
    Memo: TMemo;
    ToolBar1: TToolBar;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
    FMyQueueIn: TMyQueue;
    FMyQueueOut: TMyQueue;
    FMyThread : TMyThread;
    procedure OnIdle(Sender: TObject; var Done: Boolean);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

constructor TMyThread.Create(AMyQueueIn, AMyQueueOut : TMyQueue);
begin
  FMyQueueIn:=AMyQueueIn;
  FMyQueueOut:=AMyQueueOut;
  FPackedNumber:=0;
  inherited Create(False);
end;

procedure TMyThread.Execute;
var AMyMessage : TMyMessage;
    S : String;
begin
  while (not Terminated) do
  begin
    S:='';
    if FMyQueueIn.PopItem(AMyMessage) = TWaitResult.wrSignaled then
      S:='Из главного потока получено: ' + AMyMessage.StringMessage;
    Inc(FPackedNumber);
    with AMyMessage do
    begin
      StringMessage:=S;
      PackedNumber:=FPackedNumber;
      X:=Random(1000);
      Y:=Random(1000);
      Z:=Random(1000);
    end;
    // Отправляем Record в главную форму (главный поток)
    FMyQueueOut.PushItem(AMyMessage);
    sleep(random(800));
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FMyQueueIn:=TMyQueue.Create(30, 1000, 10);
  FMyQueueOut:=TMyQueue.Create(30, 1000, 10);;
  Application.OnIdle:=OnIdle;
  // При передаче в поток меняем местами In и Out - чтоб к контексте потока Out формы был In потока и наоборот.
  FMyThread:=TMyThread.Create(FMyQueueOut, FMyQueueIn);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FMyThread.Terminate;
  FMyThread.WaitFor;
  FMyThread.Free;
  FMyQueueIn.Free;
  FMyQueueOut.Free
end;

procedure TForm1.OnIdle(Sender: TObject; var Done: Boolean);
Var AMyMessage : TMyMessage;
    S : String;
begin
  while FMyQueueIn.PopItem(AMyMessage) = TWaitResult.wrSignaled do
  begin
    S:='Пакет№ ' + AMyMessage.PackedNumber.ToString +
      ' X=' + AMyMessage.X.ToString +
      ' Y=' + AMyMessage.Y.ToString +
      ' Z=' + AMyMessage.Z.ToString +
      ' ' + AMyMessage.StringMessage;
    Memo.BeginUpdate;
    Memo.Lines.Add(S);
    Memo.GoToTextEnd;
    Memo.EndUpdate;
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
Var AMyMessage : TMyMessage;
begin
  AMyMessage.StringMessage:='Сообщение для потока № ' + Random(100).ToString;
  FMyQueueOut.PushItem(AMyMessage);
end;

end.

 

test161.zip

Изменено пользователем Евгений Корепов

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты
  • 0

Добавлю - вместо OnIdle, очередь можно проверять по таймеру. 

В TMyMessage = record не планировал вначале добавлять StringMessage, но без него в Rio косяк - компилятор выдает ошибку https://quality.embarcadero.com/browse/RSP-21806

image.thumb.png.282b8cd642a7bc0fe7a9aab34b6b4fd4.png

Изменено пользователем Евгений Корепов

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты
  • 0
11 часов назад, ddr 2 сказал:

нужно учитывать и контролировать, что бы поток не завершился раньше, чем все "сообщения" будут обработаны основным потоком? 

Это зависит от того, какого эффекта надо добиться.
Если queue вызываете из не-анонимного потока кодом вида Self.Queue(myProcedure) - то при уничтожении потока всё что он отдал в очередь на выполнение в главный поток, будет уничтожено. Без выполнения.
Если вызывать как TThread.Queue(nil, myProcedure) - то не будет уничтожено. Но в этом случае, само собой, нельзя обращаться к полям и методам этого потока. Если требуется обращение только к полям - заведите локальные переменные и работайте с ними. Например, так:

procedure TmyThread.Execute;
var
  SomeValue, SomeAnotherValue: string;
begin
  .....
  SomeValue:=Self.FInternalValue; // Self пишу только для понимания, что Fxxx - это поле в экземпляре потока
  SomeAnotherValue:=Self.FInternalAnotherValue;
  TThread.Queue(nil, procedure
    begin
      ShowMessage(SomeValue + SomeAnotherValue); // эти переменные доживут до выполнения анонимки даже если потока уже не будет.
    end;
  );

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты
  • 0
В 06.02.2019 в 00:02, Евгений Корепов сказал:

вместо OnIdle, очередь можно проверять по таймеру

Таймер был моей самой первой реализацией, но он негативно сказывается на отзывчивости интерфейса. В качестве стресс-теста на отзывчивость - в Windows перемещение окна мышкой. Таймер вызывает лаг(и) при перемещении окна. OnIdle, я раньше не пробовал. Попробовал- работает "мягче"- лаг не замечен. Кстати этот стресс-тест создаёт "пропуск" пакетов на экране, но это не критично и решается созданием более длинной очереди(1).

В 06.02.2019 в 08:26, kami сказал:
В 05.02.2019 в 21:03, ddr 2 сказал:

нужно учитывать и контролировать, что бы поток не завершился раньше, чем все "сообщения" будут обработаны основным потоком? 

Это зависит от того, какого эффекта надо добиться.

Извините, я не правильно сформулировал вопрос. Меня заботило, то, что при закрытии формы, в момент, когда мы в FormDestroy завершаем поток TMyThread, у нас в очереди могут
быть(остаться) сообщения/пакеты, которые никогда не будут обработаны, т.е. фактически это потерянные данные. Если в TMyThread.Execute, убрать sleep(random(800)), то данный
эффект наблюдается, как в моём предыдущем варианте с подпиской на сообщения, так и в варианте Евгения. И на решении этой локальной задачи, я признал, вариант Евгения лучшим
из имеющихся, потому, что в его варианте эта задача решается достаточно просто(ниже), а в варианте с подпиской, возможно решение и существует, но я пас.
Вариант решения задачи "потерянных данных"(2), заменяем 

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FMyThread.Terminate;
  FMyThread.WaitFor;
  FMyThread.Free;
  FMyQueueIn.Free;
  FMyQueueOut.Free
end;

на 

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var TempBoolean:Boolean;
begin
  FMyThread.Terminate;
  FMyThread.WaitFor;
  onIdle(Sender,TempBoolean);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FMyThread.Free;
  FMyQueueIn.Free;
  FMyQueueOut.Free
end;

Так же для оптимизации, в TForm1.OnIdle заменил условие выполнения цикла(3).

procedure TForm1.OnIdle(Sender: TObject; var Done: Boolean);
Var AMyMessage : TMyMessage;
    S : String;
begin
  while FMyQueueIn.QueueSize>0 do
  begin
    AMyMessage:=FMyQueueIn.PopItem;
    S:='Пакет№ ' + AMyMessage.PackedNumber.ToString +
      ' X=' + AMyMessage.X.ToString +
      ' Y=' + AMyMessage.Y.ToString +
      ' Z=' + AMyMessage.Z.ToString +
      ' ' + AMyMessage.StringMessage;
    Memo.BeginUpdate;
    Memo.Lines.Add(S);
    Memo.GoToTextEnd;
    Memo.EndUpdate;
  end;
end;

В новом варианте при каждом вызове OnIdle, если в очереди(TThreadedQueue) нет "сообщений"/элементов, то происходит лишь обращение к полю и сравнение, а ранее был вызов метода TThreadedQueue.PopItem в котором выполняется много всего и большая часть в критической секции.

Вместо резюме: Всем спасибо за ответы! Для себя в решении задачи двустороннего обмена сообщениями между формой и потоком, остановился на варианте Евгения с правками (1)(2)(3)
Углубившись в проблематику, пришёл к выводу, что в кроссплатформенном FMX лучше избегать архитектуры двусторонней связи формы с потоком, а пользоваться передачей данный из 
формы в запускаемый поток, через конструктор потока, а обратно, через обработчик onTerminate.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты
  • 0

Вот вам еще пища для размышления и экспериментов. По моему мнению самый простой способ передачи из потока в главную форму:

unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
  Generics.Collections,
  System.Generics.Collections, FMX.Controls.Presentation, FMX.ScrollBox,
  FMX.Memo, FMX.StdCtrls, System.Rtti, FMX.Grid.Style, FMX.Media, FMX.Grid;

type

  TMyMessage = record
    StringMessage : String; //Я вынужден был добавить еще строку, иначе в Rio косяк https://quality.embarcadero.com/browse/RSP-21806
    PackedNumber : Integer;
    X : Integer;
    Y : Integer;
    Z : Integer;
  end;

  TOnReceiveDataEvent = procedure(const AMyMessage : TMyMessage) of object;

  TMyQueue = TThreadedQueue<TMyMessage>;

  TMyThread = class(TThread)
  protected
    FMyQueueIn : TMyQueue;
    FOnReceiveData : TOnReceiveDataEvent;
    FPackedNumber : Integer;
    procedure Execute; override;
  public
    constructor Create(AMyQueueIn : TMyQueue);
    property OnReceiveData: TOnReceiveDataEvent read FOnReceiveData write FOnReceiveData;
  end;

  TForm1 = class(TForm)
    Memo: TMemo;
    ToolBar1: TToolBar;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
    FMyQueueOut: TMyQueue;
    FMyThread : TMyThread;
    procedure OnReceiveData(const AMyMessage : TMyMessage);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

constructor TMyThread.Create(AMyQueueIn : TMyQueue);
begin
  FMyQueueIn:=AMyQueueIn;
  FPackedNumber:=0;
  inherited Create(True);
end;

procedure TMyThread.Execute;
var AMyMessage : TMyMessage;
    S : String;
begin
  while (not Terminated) do
  begin
    S:='';
    if FMyQueueIn.PopItem(AMyMessage) = TWaitResult.wrSignaled then
      S:='Из главного потока получено: ' + AMyMessage.StringMessage;
    Inc(FPackedNumber);
    with AMyMessage do
    begin
      StringMessage:=S;
      PackedNumber:=FPackedNumber;
      X:=Random(1000);
      Y:=Random(1000);
      Z:=Random(1000);
    end;
    // Отправляем Record в главную форму (главный поток)
    if Assigned(FOnReceiveData) then
      // Тут на выбор три метода доставки :-)
//      TThread.Synchronize(Self,
//      TThread.ForceQueue(Self,
      TThread.Queue(Self,
        procedure
        begin
          FOnReceiveData(AMyMessage);
        end
      );
    sleep(random(800));
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FMyQueueOut:=TMyQueue.Create(30, 1000, 10);
  FMyThread:=TMyThread.Create(FMyQueueOut);
  FMyThread.OnReceiveData:=OnReceiveData;
  FMyThread.Start;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FMyThread.Terminate;
  FMyThread.WaitFor;
  FMyThread.Free;
  FMyQueueOut.Free;
end;

procedure TForm1.OnReceiveData(const AMyMessage : TMyMessage);
Var S : String;
begin
  S:='Пакет№ ' + AMyMessage.PackedNumber.ToString +
    ' X=' + AMyMessage.X.ToString +
    ' Y=' + AMyMessage.Y.ToString +
    ' Z=' + AMyMessage.Z.ToString +
    ' ' + AMyMessage.StringMessage;
  Memo.BeginUpdate;
  Memo.Lines.Add(S);
  Memo.GoToTextEnd;
  Memo.EndUpdate;
end;

procedure TForm1.Button1Click(Sender: TObject);
Var AMyMessage : TMyMessage;
begin
  AMyMessage.StringMessage:='Сообщение для потока № ' + Random(100).ToString;
  FMyQueueOut.PushItem(AMyMessage);
end;

end.

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Для публикации сообщений создайте учётную запись или авторизуйтесь

Вы должны быть пользователем, чтобы оставить комментарий

Создать учетную запись

Зарегистрируйте новую учётную запись в нашем сообществе. Это очень просто!

Регистрация нового пользователя

Войти

Уже есть аккаунт? Войти в систему.

Войти

  • Последние посетители   0 пользователей онлайн

    Ни одного зарегистрированного пользователя не просматривает данную страницу