Приложение вызвало интерфейс, который был упорядочен для другого потока.

Я пишу приложение delphi, которое взаимодействует с Excel. Одна вещь, которую я заметил, заключается в том, что если я вызываю метод Save для объекта книги Excel, он может зависать, потому что в Excel есть диалоговое окно, открытое для пользователя. Я использую позднюю привязку.

Я хотел бы, чтобы мое приложение могло замечать, когда сохранение занимает несколько секунд, а затем выполнять какое-то действие, например, показывать диалоговое окно, сообщающее, что именно происходит.

Я подумал, что это будет довольно просто. все, что мне нужно сделать, это создать поток, который вызывает Save, и заставить этот поток вызывать подпрограмму Excel Excel. если это займет слишком много времени, я могу принять меры.

procedure TOfficeConnect.Save;
var
  Thread:TOfficeHangThread;
begin
  // spin off as thread so we can control timeout
  Thread:=TOfficeSaveThread.Create(m_vExcelWorkbook);

  if WaitForSingleObject(Thread.Handle, 5 {s} * 1000 {ms/s})=WAIT_TIMEOUT then
    begin
      Thread.FreeOnTerminate:=true;
      raise Exception.Create(_('The Office spreadsheet program seems to be busy.'));
    end;

  Thread.Free;
end;

  TOfficeSaveThread = class(TThread)
  private
    { Private declarations }
    m_vExcelWorkbook:variant;
  protected
    procedure Execute; override;
    procedure DoSave;
  public
    constructor Create(vExcelWorkbook:variant);
  end;

{ TOfficeSaveThread }

constructor TOfficeSaveThread.Create(vExcelWorkbook:variant);
begin
  inherited Create(true);

  m_vExcelWorkbook:=vExcelWorkbook;

  Resume;
end;

procedure TOfficeSaveThread.Execute;
begin
  m_vExcelWorkbook.Save;
end;

Я понимаю, что эта проблема возникает из-за того, что объект OLE был создан из другого потока (абсолютно).

как я могу обойти эту проблему? скорее всего мне нужно будет как-то "перемаршалл" для этого звонка ...

Любые идеи?


person X-Ray    schedule 16.04.2010    source источник


Ответы (5)


Вместо того, чтобы обращаться к COM-объекту из двух потоков, просто покажите диалоговое окно сообщения во вторичном потоке. VCL не является потокобезопасным, в отличие от Windows.

type
  TOfficeHungThread = class(TThread)
  private
    FTerminateEvent: TEvent;
  protected
    procedure Execute; override;
  public
   constructor Create;
   destructor Destroy; override;
   procedure Terminate; override;
  end;

...

constructor TOfficeHungThread.Create;
begin
  inherited Create(True);
  FTerminateEvent := TSimpleEvent.Create;
  Resume;
end;

destructor TOfficeHungThread.Destroy;
begin
  FTerminateEvent.Free;
  inherited;
end;

procedure TOfficeHungThread.Execute;
begin
  if FTerminateEvent.WaitFor(5000) = wrTimeout then
    MessageBox(Application.MainForm.Handle, 'The Office spreadsheet program seems to be busy.', nil, MB_OK);
end;

procedure TOfficeHungThread.Terminate;
begin
  FTerminateEvent.SetEvent;
end;

...

procedure TMainForm.Save;
var
  Thread: TOfficeHungThread;
begin
  Thread := TOfficeHungThread.Create;
  try
    m_vExcelWorkbook.Save;
    Thread.Terminate;
    Thread.WaitFor;
  finally
    Thread.Free;
  end;
end;
person Zoë Peterson    schedule 16.04.2010
comment
Проблема, похоже, в том, что в Excel открыто диалоговое окно, и он ожидает ввода данных пользователем ... - person Remko; 17.04.2010
comment
Спасибо за ваш ответ; я не думал делать это таким образом. похоже, что меня сегодня тянет в другом направлении; мне нужно будет вернуться к этому. Думаю, мне больше всего подходит это решение. - person X-Ray; 19.04.2010
comment
В конце концов, я просто помещаю в ветку сообщение, предлагающее вам посмотреть, что делает Office. работает очень красиво, спасибо! - person X-Ray; 20.04.2010

Настоящая проблема здесь в том, что приложения Office не предназначены для многопоточного использования. Поскольку может быть любое количество клиентских приложений, выдающих команды через COM, эти команды сериализуются в вызовы и обрабатываются один за другим. Но иногда Office находится в состоянии, когда он не принимает новые вызовы (например, когда отображается модальное диалоговое окно), и ваш вызов отклоняется (что дает вам ошибку «Вызов был отклонен вызываемым пользователем»). См. также ответ Джеффа Дарста в этой теме.

Что вам нужно сделать, так это реализовать IMessageFilter и позаботиться о том, чтобы ваши вызовы были отклонены. У меня так получилось:

function TIMessageFilterImpl.HandleInComingCall(dwCallType: Integer;
  htaskCaller: HTASK; dwTickCount: Integer;
  lpInterfaceInfo: PInterfaceInfo): Integer;
begin
  Result := SERVERCALL_ISHANDLED;
end;

function TIMessageFilterImpl.MessagePending(htaskCallee: HTASK;
  dwTickCount, dwPendingType: Integer): Integer;
begin
  Result := PENDINGMSG_WAITDEFPROCESS;
end;

function ShouldCancel(aTask: HTASK; aWaitTime: Integer): Boolean;
var
  lBusy: tagOLEUIBUSYA;
begin
  FillChar(lBusy, SizeOf(tagOLEUIBUSYA), 0);
  lBusy.cbStruct := SizeOf(tagOLEUIBUSYA);
  lBusy.hWndOwner := Application.Handle;

  if aWaitTime < 20000 then //enable cancel button after 20 seconds
    lBusy.dwFlags := BZ_NOTRESPONDINGDIALOG;

  lBusy.task := aTask;
  Result := OleUIBusy(lBusy) = OLEUI_CANCEL;
end;

function TIMessageFilterImpl.RetryRejectedCall(htaskCallee: HTASK;
  dwTickCount, dwRejectType: Integer): Integer;
begin
  if dwRejectType = SERVERCALL_RETRYLATER then
  begin
    if dwTickCount > 10000 then //show Busy dialog after 10 seconds
    begin
      if ShouldCancel(htaskCallee, dwTickCount) then
        Result := -1
      else
        Result := 100;
    end
    else
      Result := 100; //value between 0 and 99 means 'try again immediatly', value >= 100 means wait this amount of milliseconds before trying again
  end
  else
  begin
    Result := -1; //cancel
  end;
end;

Фильтр сообщений должен быть зарегистрирован в том же потоке, что и тот, который отправляет вызовы COM. Моя реализация фильтра сообщений будет ждать 10 секунд перед отображением стандартного диалогового окна OLEUiBusy. Это диалоговое окно дает вам возможность повторить отклоненный вызов (в вашем случае «Сохранить») или переключиться на блокирующее приложение (в Excel отображается модальное диалоговое окно). После 20 секунд блокировки кнопка отмены станет активной. Нажатие кнопки отмены приведет к сбою вашего вызова Save.

Так что забудьте возиться с потоками и внедрите фильтр сообщений, который является способом решения этих проблем.

Изменить: вышеупомянутые исправления ошибки «Вызов был отклонен вызываемым», но у вас есть «Сохранить», которое зависает. Я подозреваю, что при сохранении появляется всплывающее окно, требующее вашего внимания (у вашей книги уже есть имя файла?). Если мешает всплывающее окно, попробуйте следующее (не в отдельном потоке!):

{ Turn off Messageboxes etc. }
m_vExcelWorkbook.Application.DisplayAlerts := False;
try
  { Saves the workbook as a xls file with the name 'c:\test.xls' }
  m_vExcelWorkbook.SaveAs('c:\test.xls', xlWorkbookNormal);
finally
  { Turn on Messageboxes again }
  m_vExcelWorkbook.Application.DisplayAlerts := True;
end;

Также попробуйте выполнить отладку с помощью Application.Visible: = True; Если есть какие-либо всплывающие окна, есть изменение, вы их увидите и примете меры, чтобы предотвратить их в будущем.

person The_Fox    schedule 19.04.2010
comment
вау - какая классная возможность; я не знал ни о чем из этого! сейчас руководитель проекта хочет быстрого решения, но это показывает мне, насколько еще возможно. Спасибо - person X-Ray; 19.04.2010
comment
@ X-Ray: Я должен признать, что это не может быть решением вашей проблемы, теперь я снова читаю ваш пост. В случае, если вызов был отклонен вызываемым пользователем, вызов метода немедленно завершается ошибкой и не зависает. Когда вы говорите «Сохранить», я подозреваю, что есть диалог, требующий вашего внимания. См. Мой отредактированный ответ для получения дополнительной информации. - person The_Fox; 20.04.2010
comment
@The_Fox, это действительно очень информативный ответ, спасибо! Моя проблема связана с открытием документа Word с использованием автоматизации OLE (с использованием WordXp.pas) - поскольку есть много причин, по которым Word будет отображать диалоговое окно при открытии документа, как вы думаете, это есть универсальный способ справиться с этой ситуацией? Я имею в виду, я хочу добиться: всякий раз, когда появляется диалоговое окно, выводить его перед пользователем, позволять пользователю выполнять действия, чтобы документ успешно открылся в конце. - person Edwin Yip; 13.12.2019
comment
@EdwinYip: В диалоговом окне OLEUIBusy есть кнопка переключения. Но заранее единственное, что вы можете сделать, - это сделать Word видимым, вывести его на передний план и начать автоматизацию. Другого пути я не знаю. - person The_Fox; 18.12.2019

Попробуйте вызвать CoInitializeEx с COINIT_MULTITHREADED, поскольку в MSDN указано:

Многопоточность (также называемая свободным потоком) позволяет вызовам методов объектов, созданных этим потоком, выполняться в любом потоке.

person Remko    schedule 17.04.2010
comment
Спасибо за ваш ответ; я не знал об этом! похоже, что меня сегодня тянет в другом направлении; мне нужно будет вернуться к этому. - person X-Ray; 19.04.2010
comment
@Bob: Но, безусловно, многопоточное приложение может использовать (однопоточные) com-объекты, и, как я читал, это именно то, о чем просили: однопоточный com-объект делает вызывающее приложение невосприимчивым, и поэтому требуется второй поток. - person Remko; 05.06.2010

«Маршалинг» интерфейса от одного потока к другому можно выполнить с помощью CoMarshalInterThreadInterfaceInStream, чтобы поместить интерфейс в поток, переместить поток в другой поток и затем использовать CoGetInterfaceAndReleaseStream, чтобы вернуть интерфейс из потока. см. пример здесь в Delphi.

person Lars Truijens    schedule 18.04.2010
comment
спасибо за вашу помощь по марсаллингу! похоже, что меня сегодня тянет в другом направлении; мне нужно будет вернуться к этому. - person X-Ray; 19.04.2010

Я думаю, что ответ Ларса правильный. Альтернативой его предложению является использование GIT (Global Interface Table), который можно использовать в качестве межпоточного репозитория для интерфейсов.

См. Эту тему SO здесь для кода для взаимодействие с GIT, где я разместил модуль Delphi, обеспечивающий простой доступ к GIT.

Это должен быть просто вопрос регистрации вашего интерфейса Excel в GIT из вашего основного потока, а затем получения отдельной ссылки на интерфейс из вашего потока TOfficeHangThread с помощью метода GetInterfaceFromGlobal.

person Conor Boyd    schedule 18.04.2010
comment
Спасибо за ваш ответ; Я думаю, что выберу самое простое и легкое решение, поскольку менеджеру проекта нужно быстрое решение. - person X-Ray; 19.04.2010