unit Bs1DeviceControl; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes, ShellAPI, Vcl.Graphics, System.StrUtils, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, SuperObject, System.Generics.Collections, DefineHelper, GlobalDefine, Bs1FltCtrl, Tocsg.Path, Bs1PolicyUnit, DataFlowSettingForm, Bs1MadHookInject, MessageBoxFrom, DeviceGuard.Logic, ProcessCreateSettingForm; const // 윈도우 장치 변경 메시지 상수 DBT_DEVNODES_CHANGED = $0007; WM_DEVICECHANGE = $0219; WM_REFRESH_VIEW = WM_USER + 3951; WM_POPUP_MSG2 = WM_REFRESH_VIEW + 1; type TScrollablePanel = class(TPanel) public property OnMouseWheel; end; TUIItem = class public Panel: TPanel; RgControl: TRadioGroup; RgLog: TRadioGroup; Flag: DWORD; constructor Create(APanel: TPanel; ARgControl, ARgLog: TRadioGroup; AFlag: DWORD); end; TForm1 = class(TForm) PanelTop: TPanel; MemoLog: TMemo; ScrollBoxDevices: TScrollBox; Splitter1: TSplitter; ButtonAllEnumDevice: TButton; ButtonRunDeviceManager: TButton; btnDataFlowConfig: TButton; btnOpenRegedit: TButton; grpUsbPortExcept: TGroupBox; lblVendor: TLabel; lblProduct: TLabel; lblSerial: TLabel; edtVendor: TEdit; edtProduct: TEdit; edtSerial: TEdit; btnUsbPortExceptAdd: TButton; btnPortExceptDel: TButton; btnLogClear: TButton; btnDataFlowStart: TButton; btnProcessCreateSetting: TButton; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure BtnApplyClick(Sender: TObject); procedure ButtonAllEnumDeviceClick(Sender: TObject); procedure PanelMouseWheelHandler(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean); procedure ButtonRunDeviceManagerClick(Sender: TObject); procedure btnDriveControlClick(Sender: TObject); procedure btnDataFlowConfigClick(Sender: TObject); procedure btnOpenRegeditClick(Sender: TObject); procedure btnUsbPortExceptAddClick(Sender: TObject); procedure btnPortExceptDelClick(Sender: TObject); procedure btnLogClearClick(Sender: TObject); procedure OnDataFlowStartClick(Sender: TObject); procedure btnProcessCreateSettingClick(Sender: TObject); private FEngine: TDeviceGuardEngine; FUIList: TObjectList; Fbs1MadHookInject_: TBs1MadHookInject; procedure WMDeviceChange(var Msg: TMessage); message WM_DEVICECHANGE; procedure BuildUIFromEngine; procedure AddDeviceUI(const Name: string; Flag: DWORD; state : TDeviceState; log: Boolean; IsBT: Boolean = False); procedure OnEngineLog(const Msg: string); procedure OnEnginePopup(const Msg: string); procedure OnUIChange(Sender: TObject); procedure ModelessClose(Sender: TObject; var Action: TCloseAction); procedure ShowMessageModeless(const Msg: string); procedure process_WM_COPYDATA(var msg: TMessage); Message WM_COPYDATA; procedure process_WM_POPUP_MSG2(var msg: TMessage); Message WM_POPUP_MSG2; public class function ReportCallback(Context: Pointer): DWORD; stdcall; static; protected procedure CreateParams(var Params: TCreateParams); override; public { Public declarations } end; var Form1: TForm1; implementation {$R *.dfm} uses DriveControlForm; { TUIItem } class function TForm1.ReportCallback(Context: Pointer): DWORD; stdcall; var logMsg: string; // Instance: TDeviceGuardEngine; begin if Context = nil then Exit; LogMsg := string(PWideChar(Context)); OutputDebugString(PWideChar(Context)); TThread.Queue(nil, procedure begin if Assigned(Form1) then begin Form1.OnEngineLog(LogMsg); end; end); Result := 0; end; constructor TUIItem.Create(APanel: TPanel; ARgControl, ARgLog: TRadioGroup; AFlag: DWORD); begin Panel := APanel; RgControl := ARgControl; RgLog := ARgLog; Flag := AFlag; end; { TForm1 } procedure TForm1.ShowMessageModeless(const Msg: string); var Dlg: TForm; i: Integer; begin // 1. 메시지 다이얼로그 폼 생성 (화면에 띄우지는 않음) Dlg := CreateMessageDialog(Msg, mtInformation, [mbOK]); Dlg.FormStyle := fsStayOnTop; Dlg.Color := clBlue; for i := 0 to Dlg.ComponentCount - 1 do begin // 메시지 텍스트(TLabel) 찾기 if Dlg.Components[i] is TLabel then TLabel(Dlg.Components[i]).Font.Color := clWhite; end; Dlg.OnClose := ModelessClose; Dlg.Show; end; procedure TForm1.ModelessClose(Sender: TObject; var Action: TCloseAction); begin Action := caFree; // 폼이 닫힐 때 메모리 해제 end; procedure TForm1.CreateParams(var Params: TCreateParams); begin inherited CreateParams(Params); // 델파이 기본 클래스명(TfrmReceiver) 대신 내가 원하는 이름으로 변경 // StrCopy를 사용하여 PChar 배열에 복사해야 합니다. StrCopy(Params.WinClassName, PChar(BS1DC_CLASS_NAME)); end; procedure TForm1.FormCreate(Sender: TObject); //var // LMessageBoxForm: TMessageBoxForm; var path: string; state: DWORD; begin Caption := 'Advanced Device Guard (Event & Flag Based)'; path:= GetRunExePathDir; //ScrollBoxDevices.Align := alLeft; // 왼쪽 정렬 (창 늘려도 너비 유지) // ScrollBoxDevices.Width := 600; // 원하는 너비로 고정 (예: 600px) // 앵커 설정: 위/아래로 폼 크기 변경 시 높이는 따라가되, 너비는 고정akBottom ScrollBoxDevices.Anchors := [akLeft, akTop]; btnDataFlowStart.Caption := '파일 유출 제어 시작'; btnDataFlowStart.Tag := 1; FUIList := TObjectList.Create; gBs1fltControl:= TBs1fltControl.Create; try // 커널 드라이버의 위치를 정한다. state := gBs1fltControl.InitDriver(path, TForm1.ReportCallback); if state = 0 then begin OnEngineLog('Bs1FltCtrl 초기화 성공'); gBs1fltControl.BeginControl(1); gBs1FltControl.SetDeviceProtect(1); gBs1FltControl.Debug(3); gBs1FltControl.SetHook(DWORD(BDC_USB), 1); gBs1FltControl.SetHook(DWORD(BDC_BLUETOOTH), 1); //gBs1FltControl.SetHook(DWORD(BDC_MTP), 1); end else begin OnEngineLog('Bs1FltCtrl 초기화 실패: ' + IntToStr(state)); end; finally end; FEngine := TDeviceGuardEngine.Create; FEngine.OnLog := OnEngineLog; FEngine.OnPopup := OnEnginePopup; FEngine.TriggerScan; ChangeWindowMessageFilter(WM_COPYDATA, MSGFLT_ADD); BuildUIFromEngine; FEngine.Start; Fbs1MadHookInject_:= TBs1MadHookInject.Create; Fbs1MadHookInject_.OnLog := OnEngineLog; Fbs1MadHookInject_.StartInject(); for var Policy in gBs1Policy.Policies do begin // Flag에 따라 적절한 콤보박스 매핑 case TDeviceType(Policy.flag_) of BDC_CDROM, BDC_FLOOPY, BDC_USB_DISK, BDC_NETWORKDRIVEOUT, BDC_NETWORKDRIVEIN, BDC_EXTERNALHDD: gBs1FltControl.SetPolicy(DWORD(Policy.flag_), DWORD(Policy.state_), DWORD(Policy.isLog_)); end; end; OnEngineLog('감시 엔진이 시작되었습니다 (Event 대기 중).'); end; procedure TForm1.FormDestroy(Sender: TObject); begin OutputDebugStringW('[Bs1dc] Destory!!!++++++++++++++'); if Assigned(Fbs1MadHookInject_) then begin Fbs1MadHookInject_.StopInject(); FreeAndNil(Fbs1MadHookInject_); end; FEngine.Stop; FEngine.Free; FUIList.Free; if gBs1fltControl <> nil then begin gBs1fltControl.BeginControl(0); gBs1fltControl.Cleanup; FreeAndNil(gBs1fltControl); end; OutputDebugStringW('[Bs1dc] Destory!!!--------------'); ShowMessage('종료 되었습니다.'); end; procedure TForm1.BuildUIFromEngine; var i: Integer; begin // 1. FUIList 초기화 (TUIItem 객체들 해제) FUIList.Clear; // 2. ScrollBox 내의 기존 컨트롤(패널들) 제거 // 역순으로 지워야 인덱스 오류가 안 납니다. if ScrollBoxDevices <> nil then begin // ScrollBoxDevices의 스크롤 위치 초기화 (UI 깨짐 방지) ScrollBoxDevices.VertScrollBar.Position := 0; for i := ScrollBoxDevices.ControlCount - 1 downto 0 do begin // TScrollablePanel만 골라서 제거 if ScrollBoxDevices.Controls[i] is TPanel then ScrollBoxDevices.Controls[i].Free; end; end; // 3. 정책 개수 확인 (디버깅용) // if FEngine.Policies.Count = 0 then // ShowMessage('표시할 정책(장치)이 없습니다.'); // 4. UI 생성 루프 for var Policy in gBs1Policy.Policies do begin case Policy.flag_ of DWORD(BDC_CDROM), DWORD(BDC_FLOOPY), DWORD(BDC_USB_DISK), DWORD(BDC_NETWORKDRIVEOUT), DWORD(BDC_NETWORKDRIVEIN), DWORD(BDC_EXTERNALHDD): continue; end; AddDeviceUI(Policy.Name, Policy.flag_, Policy.state_, Policy.isLog_, Policy.isBluetooth_); // 방금 추가된 UI 아이템 가져오기 // if FUIList.Count > 0 then // begin // var LastUI := FUIList.Last; // // // 제어 상태 반영 (ItemIndex: 0=사용, 1=차단) // if Policy.state_ = dsDisable then // LastUI.RgControl.ItemIndex := 1 // else // LastUI.RgControl.ItemIndex := 0; // // // 로그 상태 반영 (ItemIndex: 0=미사용, 1=사용) // if Policy.isLog_ then // LastUI.RgLog.ItemIndex := 1 // else // LastUI.RgLog.ItemIndex := 0; OnEngineLog(Format('BuildUIFromEngine, state_(%d), isLog_(%d)', [DWORD(Policy.state_), DWORD(Policy.isLog_)])); // end; end; end; procedure TForm1.WMDeviceChange(var Msg: TMessage); begin inherited; if Msg.WParam = DBT_DEVNODES_CHANGED then begin OnEngineLog('[System] 하드웨어 변경 감지 -> 재검사 요청'); FEngine.TriggerScan; end; end; procedure TForm1.process_WM_POPUP_MSG2(var msg: TMessage); begin TMessageBoxForm.Create(nil, string(msg.LParam)).Show; end; procedure TForm1.process_WM_COPYDATA(var msg: TMessage); var sReceivedText: string; dwData: DWORD; pCpData: PCopyDataStruct; O: ISuperObject; begin msg.Result := 0; pCpData := PCopyDataStruct(msg.LParam); OnEngineLog('process_WM_COPYDATA'); try dwData := pCpData.dwData; OnEngineLog('process_WM_COPYDATA : ' + dwData.ToString); case dwData of HPCMD_FILE_OPERATION_NOTI: begin O := SO(Copy(PChar(pCpData.lpData), 1, pCpData.cbData)); if O.B['N'] then begin var Code: DWORD; var Summary: string; var sDstPath: string; var sSrcPath: string; var deviceName: string; var evt: Integer; OnEngineLog('process_WM_COPYDATA : HPCMD_FILE_OPERATION_NOTI'); evt:= O.I['E']; sSrcPath:= O.S['S']; sDstPath:= O.S['D']; deviceName:= O.S['T']; Summary:= '[Folder]' + ExtractFileName(O.S['D']); OnEngineLog(Format('[FILE_OPER] (%s)(%s), (%d), (%s)->(%s)', [deviceName, Summary, DWORD(evt), sSrcPath, sDstPath])); if O.B['B'] then begin SendMessage(Handle, WM_POPUP_MSG2, 0, NativeInt(O.AsString)); end; // begin // Code:= PREVENT_NETFOLDER_FILE; // Summary:= 'Block : '; // // if MgSvc_.ModePolicy.ShFileCrMon.bBlkNoti then // MgSvc_.PopupMessage(TYPE_MSG_PREVENT_FILEOPER, O.AsJSon); // end else begin // Code:= MONITOR_NETFOLDER_FILE; // Summary:= 'Monitor : '; // // if MgSvc_.ModePolicy.ShFileCrMon.bMonNoti then // MgSvc_.PopupMessage(TYPE_MSG_MONITOR_FILEOPER, O.AsJSon); // end; // LogInfo.sSummary := LogInfo.sSummary + ExtractFileName(O.S['D']); // LogInfo.sAppName := 'explorer.exe'; // LogInfo.sPath := O.S['D']; // MgSvc_.SendEventLogEx(@LogInfo, O.B['B']); end; msg.Result := 1; end; end; except on E: Exception do OnEngineLog('process_WM_COPYDATA except'); // ETgException.TraceException(Self, E, 'Fail .. process_WM_CTTSCH_REQUEST()'); end; // // // //case dwData of // // 1. 전달받은 데이터가 있는지 확인 // if Msg.CopyDataStruct.lpData <> nil then // begin // // 2. lpData 포인터에서 문자열을 가져옴 // // (보내는 쪽에서 Null-Terminated String으로 보냈다고 가정) // sReceivedText := PChar(Msg.CopyDataStruct.lpData); // // // 3. 화면에 표시 // OnEngineLog(Format('[%s] %s', [FormatDateTime('hh:mm:ss', Now), sReceivedText])); // // // 4. 처리 결과 반환 (일반적으로 처리가 잘 되었으면 1 또는 True 반환) // Msg.Result := 1; // end // else // begin // Msg.Result := 0; // end; end; // 정책 저장 버튼 procedure TForm1.BtnApplyClick(Sender: TObject); begin gBs1Policy.SavePolicyToFile; OnEngineLog('정책 저장 완료.'); FEngine.TriggerScan; end; procedure TForm1.ButtonAllEnumDeviceClick(Sender: TObject); begin FEngine.AllEnumSystemDevice; end; procedure TForm1.btnOpenRegeditClick(Sender: TObject); begin ShellExecute( 0, 'open', 'regedit.exe', nil, nil, SW_SHOWNORMAL ); end; function StrToHexOrInt(const StrValue: string): Integer; var S: string; begin S := Trim(StrValue); // 앞뒤 공백 제거 if TryStrToInt(S, Result) then Exit; if StartsText('0x', S) then begin S := StringReplace(S, '0x', '$', [rfIgnoreCase]); if TryStrToInt(S, Result) then Exit; end; if TryStrToInt('$' + S, Result) then Exit; Result := 0; end; procedure TForm1.btnPortExceptDelClick(Sender: TObject); var InputStrVid: string; InputStrPid: string; LsVid: string; LsPid: string; LSerial: string; LProdoct: string; state: DWORD; LVid: DWORD; LPid: DWORD; begin InputStrVid:= edtVendor.Text; InputStrPid:= edtProduct.Text; LSerial:= edtSerial.Text; if StartsText('0x', InputStrVid) then begin LsVid := Copy(InputStrVid, 3, Length(InputStrVid)); LVid := StrToInt('$' + LsVid); end else begin LsVid := InputStrVid; LVid := StrToInt(LsVid); end; if StartsText('0x', InputStrPid) then begin LsPid := Copy(InputStrPid, 3, Length(InputStrPid)); LPid := StrToInt('$' + LsPid); end else begin LsPid := InputStrPid; LPid := StrToInt(LsPid); end; gBs1Policy.RemoveUsbPortExcept(LsVid, LsPid, LSerial); state:= gBs1fltControl.DelUsbPortException(LVid, LPid, 0, PWideChar(LSerial)); if state = 0 then begin ShowMessage('제거 되었습니다'); end else ShowMessage('제거 실패 하였습니다.'); end; procedure TForm1.btnProcessCreateSettingClick(Sender: TObject); var dlg: TFormProcessCreateSetting; begin dlg := TFormProcessCreateSetting.Create(Self); try dlg.OnLog := OnEngineLog; dlg.ShowModal; finally dlg.Free; end; end; procedure TForm1.btnUsbPortExceptAddClick(Sender: TObject); var InputStrVid: string; InputStrPid: string; LsVid: string; LsPid: string; LSerial: string; LProdoct: string; state: DWORD; LVid: DWORD; LPid: DWORD; begin InputStrVid:= edtVendor.Text; InputStrPid:= edtProduct.Text; LSerial:= edtSerial.Text; if StartsText('0x', InputStrVid) then begin LsVid := Copy(InputStrVid, 3, Length(InputStrVid)); LVid := StrToInt('$' + LsVid); end else begin LsVid := InputStrVid; LVid := StrToInt(LsVid); end; if StartsText('0x', InputStrPid) then begin LsPid := Copy(InputStrPid, 3, Length(InputStrPid)); LPid := StrToInt('$' + LsPid); end else begin LsPid := InputStrPid; LPid := StrToInt(LsPid); end; // LVid:= StrToHexOrInt(LVendor); // LPid:= StrToHexOrInt(LProdoct); gBs1Policy.AddUsbPortExcept(LsVid, LsPid, LSerial); state:= gBs1fltControl.SetUsbPortException(LVid, LPid, 0, PWideChar(LSerial)); if state = 0 then begin ShowMessage('추가 되었습니다'); end else ShowMessage('추가 실패 하였습니다.'); end; procedure TForm1.ButtonRunDeviceManagerClick(Sender: TObject); begin ShellExecute( 0, 'open', 'devmgmt.msc', nil, nil, SW_SHOWNORMAL ); end; procedure TForm1.OnUIChange(Sender: TObject); var Rg: TRadioGroup; NewState: TDeviceState; NewLogState: Boolean; begin Rg := Sender as TRadioGroup; for var UI in FUIList do begin if (UI.RgControl = Rg) or (UI.RgLog = Rg) then begin // 현재 UI의 상태 읽기 if UI.RgControl.ItemIndex = 1 then NewState := dsDisable else NewState := dsEnable; if UI.RgLog.ItemIndex = 1 then NewLogState := True else NewLogState := False; // 엔진 업데이트 호출 (UpdatePolicyState 시그니처 수정 필요할 수 있음) gBs1Policy.UpdatePolicyState(UI.Flag, NewState, NewLogState); Break; end; end; end; procedure TForm1.AddDeviceUI(const Name: string; Flag: DWORD; state : TDeviceState; log: Boolean; IsBT: Boolean); var Pnl: TPanel; // TScrollablePanel 대신 TPanel 사용 권장 (가볍음) RightPnl: TPanel; RgCtrl, RgLog: TRadioGroup; Lbl: TLabel; // 배율 적용을 위한 변수 ScaleF: Single; RadioWidth: Integer; RightPanelWidth: Integer; PanelHeight: Integer; begin if ScrollBoxDevices = nil then Exit; // 1. [핵심] 현재 화면 배율 계산 (기본 96 DPI 기준) // 델파이 최신 버전(10.x 이상)에서는 ScaleFactor 속성을 지원합니다. // 구버전이라면 GetDeviceCaps(Canvas.Handle, LOGPIXELSX) / 96.0; 을 쓰세요. ScaleF := Self.ScaleFactor; // 2. [핵심] 고정값에 배율 곱하기 RadioWidth := Round(120 * ScaleF); // 120 -> 240 (200%일 때) RightPanelWidth := (RadioWidth * 2) + Round(20 * ScaleF); PanelHeight := Round(55 * ScaleF); // 3. 메인 행 패널 생성 Pnl := TPanel.Create(ScrollBoxDevices); Pnl.Parent := ScrollBoxDevices; Pnl.Align := alTop; Pnl.Height := PanelHeight; // 높이도 배율 적용 Pnl.BevelOuter := bvNone; Pnl.BevelKind := bkTile; Pnl.BevelEdges := [beBottom]; Pnl.AlignWithMargins := True; Pnl.Margins.SetBounds(2, 2, 2, 0); Pnl.ParentBackground := False; Pnl.Color := clWhite; // Panel은 캡션이 가운데 뜨므로 지워줌 Pnl.Caption := ''; Pnl.Visible := True; // 4. 오른쪽 버튼 컨테이너 패널 RightPnl := TPanel.Create(Pnl); RightPnl.Parent := Pnl; RightPnl.Align := alRight; RightPnl.Width := RightPanelWidth; // 너비도 배율 적용 RightPnl.BevelOuter := bvNone; RightPnl.ParentBackground := False; RightPnl.Color := clWhite; RightPnl.Caption := ''; RightPnl.Margins.Right := Round(10 * ScaleF); RightPnl.AlignWithMargins := True; // 5. 로그 라디오 그룹 (오른쪽) RgLog := TRadioGroup.Create(RightPnl); RgLog.Parent := RightPnl; RgLog.Caption := '로그'; RgLog.Align := alRight; RgLog.Width := RadioWidth; // 너비 배율 적용 RgLog.Columns := 2; RgLog.Items.Add('OFF'); RgLog.Items.Add('ON'); // 로그 상태 반영 (ItemIndex: 0=미사용, 1=사용) if log then RgLog.ItemIndex := 1 else RgLog.ItemIndex := 0; RgLog.OnClick := OnUIChange; // 이벤트 연결 필요하면 주석 해제 RgLog.Margins.SetBounds(Round(5 * ScaleF), Round(2 * ScaleF), 0, Round(2 * ScaleF)); RgLog.AlignWithMargins := True; // 6. 제어 라디오 그룹 (왼쪽 채우기) RgCtrl := TRadioGroup.Create(RightPnl); RgCtrl.Parent := RightPnl; RgCtrl.Caption := '제어'; RgCtrl.Align := alClient; RgCtrl.Columns := 2; RgCtrl.Items.Add('허용'); RgCtrl.Items.Add('차단'); if state = dsDisable then RgCtrl.ItemIndex := 1 else RgCtrl.ItemIndex := 0; // RgCtrl.ItemIndex := 0; RgCtrl.OnClick := OnUIChange; // 이벤트 연결 필요하면 주석 해제 RgCtrl.Margins.SetBounds(0, Round(2 * ScaleF), Round(5 * ScaleF), Round(2 * ScaleF)); RgCtrl.AlignWithMargins := True; // 7. 장치 이름 라벨 (왼쪽) Lbl := TLabel.Create(Pnl); Lbl.Parent := Pnl; Lbl.Caption := Name; Lbl.AutoSize := False; Lbl.Align := alClient; Lbl.Layout := tlCenter; Lbl.Font.Size := 9; // 폰트 크기는 OS가 알아서 배율 적용함 (보통 안 건드려도 됨) Lbl.Font.Style := [fsBold]; Lbl.Margins.Left := Round(15 * ScaleF); Lbl.AlignWithMargins := True; Lbl.Transparent := True; // 리스트에 추가 FUIList.Add(TUIItem.Create(Pnl, RgCtrl, RgLog, Flag)); end; procedure TForm1.OnDataFlowStartClick(Sender: TObject); begin // if btnDataFlowStart.Tag = 0 then begin btnDataFlowStart.Caption := '파일 유출 제어 시작'; btnDataFlowStart.Tag := 1; Fbs1MadHookInject_.StopInject(); end else begin btnDataFlowStart.Caption := '파일 유출 제어 중지'; btnDataFlowStart.Tag := 0; Fbs1MadHookInject_.StartInject(); end; end; procedure TForm1.OnEngineLog(const Msg: string); begin MemoLog.Lines.BeginUpdate; try MemoLog.Lines.Add(Format('[%s] %s', [FormatDateTime('hh:nn:ss', Now), Msg])); // 스크롤도 여기서 처리해야 안전함 SendMessage(MemoLog.Handle, WM_VSCROLL, SB_BOTTOM, 0); finally MemoLog.Lines.EndUpdate; end; // TThread.Queue(nil, procedure // var // LForm: TMessageBoxForm; // begin // // 1. 메모 로그 출력 (이제 안전함) // MemoLog.Lines.BeginUpdate; // try // MemoLog.Lines.Add(Format('[%s] %s', [FormatDateTime('hh:nn:ss', Now), Msg])); // // // 스크롤도 여기서 처리해야 안전함 // SendMessage(MemoLog.Handle, WM_VSCROLL, SB_BOTTOM, 0); // finally // MemoLog.Lines.EndUpdate; // end; // // // 2. 특정 조건 시 메시지 창 띄우기 // if ContainsText(Msg, 'MATCHING!!!!!BLOCK!!!!!') then // begin // LForm := TMessageBoxForm.Create(Application); // try // LForm.lblMessage.Caption := Msg; // LForm.Show; // except // LForm.Free; // end; // end; // end); end; procedure TForm1.OnEnginePopup(const Msg: string); begin self.Caption := '경고: ' + Msg; end; procedure TForm1.PanelMouseWheelHandler(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean); var ScrollCode: Integer; begin if WheelDelta > 0 then ScrollCode := SB_LINEUP else ScrollCode := SB_LINEDOWN; PostMessage(ScrollBoxDevices.Handle, WM_VSCROLL, ScrollCode, 0); Handled := True; end; procedure TForm1.btnDataFlowConfigClick(Sender: TObject); var dlg: TDataFlowSettingForm; begin dlg := TDataFlowSettingForm.Create(Self); try dlg.SetEngine(gBs1Policy); if dlg.ShowModal = mrOk then begin OnEngineLog('DataFlow 설정이 변경되었습니다.'); end; finally dlg.Free; end; end; procedure TForm1.btnDriveControlClick(Sender: TObject); var DriveForm: TDriveControlForm; begin DriveForm := TDriveControlForm.Create(Self); try // 엔진 참조 전달 (필요 시) DriveForm.Engine := FEngine; DriveForm.OnLog := OnEngineLog; DriveForm.LoadSettings; DriveForm.ShowModal; finally DriveForm.Free; end; end; procedure TForm1.btnLogClearClick(Sender: TObject); begin MemoLog.Lines.Clear; end; end.