Blockly test
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
program BlocklyTest;
|
||||
|
||||
uses
|
||||
System.StartUpCopy,
|
||||
FMX.Forms,
|
||||
MainUnit in 'MainUnit.pas' {MainForm},
|
||||
LLVM.Runner in 'LLVM.Runner.pas';
|
||||
|
||||
{$R *.res}
|
||||
|
||||
begin
|
||||
Application.Initialize;
|
||||
Application.CreateForm(TMainForm, MainForm);
|
||||
Application.Run;
|
||||
end.
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,68 @@
|
||||
unit LLVM.Runner;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
System.SysUtils, Winapi.Windows, System.Classes;
|
||||
|
||||
type
|
||||
// Callback-Prozedur, die von der DLL aufgerufen wird
|
||||
TLogIntegerCallback = procedure( AValue: Integer ); stdcall;
|
||||
|
||||
// Signatur der exportierten DLL-Funktion
|
||||
TStarteLogikProc = procedure( ALogCallback: TLogIntegerCallback ); stdcall;
|
||||
|
||||
type
|
||||
TLLVMRunner = class
|
||||
private
|
||||
FModule: HMODULE;
|
||||
FStarteLogik: TStarteLogikProc;
|
||||
class var
|
||||
FLog: TStrings;
|
||||
class procedure LogInteger( AValue: Integer ); static; stdcall;
|
||||
public
|
||||
constructor Create(const ADLLPath: string);
|
||||
destructor Destroy; override;
|
||||
procedure Execute;
|
||||
class property Log: TStrings read FLog write FLog;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
{ TLLVMRunner }
|
||||
|
||||
constructor TLLVMRunner.Create(const ADLLPath: string);
|
||||
begin
|
||||
inherited Create;
|
||||
FModule := LoadLibrary( PChar( ADLLPath ) );
|
||||
if FModule = 0 then
|
||||
raise Exception.Create( 'Failed to load DLL.' );
|
||||
|
||||
FStarteLogik := GetProcAddress( FModule, 'StarteLogik' );
|
||||
if not Assigned( FStarteLogik ) then
|
||||
raise Exception.Create( 'Procedure "StarteLogik" not found in DLL.' );
|
||||
end;
|
||||
|
||||
destructor TLLVMRunner.Destroy;
|
||||
begin
|
||||
if FModule <> 0 then
|
||||
FreeLibrary( FModule );
|
||||
inherited;
|
||||
end;
|
||||
|
||||
procedure TLLVMRunner.Execute;
|
||||
begin
|
||||
if Assigned( FStarteLogik ) then
|
||||
// Führe die DLL-Funktion aus und übergebe unsere Callback-Methode
|
||||
FStarteLogik( LogInteger );
|
||||
end;
|
||||
|
||||
// Dies ist die Methode, die die DLL aufruft.
|
||||
// Sie muss "static" (class procedure) sein, um eine feste Adresse zu haben.
|
||||
class procedure TLLVMRunner.LogInteger( AValue: Integer );
|
||||
begin
|
||||
if Assigned( FLOg ) then
|
||||
FLog.Add( Format( 'DLL logged integer: %d', [AValue] ) );
|
||||
end;
|
||||
|
||||
end.
|
||||
@@ -0,0 +1,68 @@
|
||||
object MainForm: TMainForm
|
||||
Left = 0
|
||||
Top = 0
|
||||
Caption = 'Form1'
|
||||
ClientHeight = 872
|
||||
ClientWidth = 1010
|
||||
FormFactor.Width = 320
|
||||
FormFactor.Height = 480
|
||||
FormFactor.Devices = [Desktop]
|
||||
OnCreate = FormCreate
|
||||
OnDestroy = FormDestroy
|
||||
DesignerMasterStyle = 0
|
||||
object WebBrowser1: TWebBrowser
|
||||
Align = Top
|
||||
Anchors = [akLeft, akTop, akRight, akBottom]
|
||||
Size.Width = 1010.000000000000000000
|
||||
Size.Height = 633.000000000000000000
|
||||
Size.PlatformDefault = False
|
||||
WindowsEngine = EdgeOnly
|
||||
OnDidFinishLoad = WebBrowser1DidFinishLoad
|
||||
end
|
||||
object LauchSiteButton: TButton
|
||||
Anchors = [akLeft, akBottom]
|
||||
Position.X = 24.000000000000000000
|
||||
Position.Y = 744.000000000000000000
|
||||
TabOrder = 1
|
||||
Text = 'Lauch Site'
|
||||
TextSettings.Trimming = None
|
||||
OnClick = LauchSiteButtonClick
|
||||
end
|
||||
object SaveWorkspaceButton: TButton
|
||||
Anchors = [akLeft, akBottom]
|
||||
Position.X = 24.000000000000000000
|
||||
Position.Y = 774.000000000000000000
|
||||
Size.Width = 97.000000000000000000
|
||||
Size.Height = 22.000000000000000000
|
||||
Size.PlatformDefault = False
|
||||
TabOrder = 2
|
||||
Text = 'Save Workspace'
|
||||
TextSettings.Trimming = None
|
||||
OnClick = SaveWorkspaceButtonClick
|
||||
end
|
||||
object GenerateCodeButton: TButton
|
||||
Anchors = [akLeft, akBottom]
|
||||
Position.X = 24.000000000000000000
|
||||
Position.Y = 804.000000000000000000
|
||||
Size.Width = 97.000000000000000000
|
||||
Size.Height = 22.000000000000000000
|
||||
Size.PlatformDefault = False
|
||||
TabOrder = 3
|
||||
Text = 'Generate Code'
|
||||
TextSettings.Trimming = None
|
||||
OnClick = GenerateCodeButtonClick
|
||||
end
|
||||
object Memo: TMemo
|
||||
Touch.InteractiveGestures = [Pan, LongTap, DoubleTap]
|
||||
DataDetectorTypes = []
|
||||
Anchors = [akRight, akBottom]
|
||||
Position.X = 144.000000000000000000
|
||||
Position.Y = 641.000000000000000000
|
||||
Size.Width = 841.000000000000000000
|
||||
Size.Height = 223.000000000000000000
|
||||
Size.PlatformDefault = False
|
||||
TabOrder = 4
|
||||
Viewport.Width = 837.000000000000000000
|
||||
Viewport.Height = 219.000000000000000000
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,314 @@
|
||||
unit MainUnit;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
|
||||
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Controls.Presentation, FMX.StdCtrls, FMX.WebBrowser,
|
||||
Winapi.WebView2, FMX.Memo.Types, FMX.ScrollBox, FMX.Memo,
|
||||
// Korrekte und vollständige Units
|
||||
System.IOUtils, System.Threading, System.Diagnostics, Winapi.Windows, LLVM.Runner;
|
||||
|
||||
const
|
||||
// Pfad zur HTML-Datei
|
||||
PATH_BLOCKLY = 'T:\Myc\Blockly\blockly_LLVM.html';
|
||||
|
||||
// Pfade zu den LLVM-Werkzeugen
|
||||
CLANG_PATH = 'clang.exe';
|
||||
LLD_PATH = 'lld-link.exe';
|
||||
|
||||
type
|
||||
TMainForm = class( TForm )
|
||||
WebBrowser1: TWebBrowser;
|
||||
LauchSiteButton: TButton;
|
||||
SaveWorkspaceButton: TButton;
|
||||
GenerateCodeButton: TButton;
|
||||
Memo: TMemo;
|
||||
procedure FormCreate( Sender: TObject );
|
||||
procedure FormDestroy( Sender: TObject );
|
||||
procedure LauchSiteButtonClick( Sender: TObject );
|
||||
procedure SaveWorkspaceButtonClick( Sender: TObject );
|
||||
procedure GenerateCodeButtonClick( Sender: TObject );
|
||||
procedure WebBrowser1DidFinishLoad( ASender: TObject );
|
||||
private
|
||||
{ Private declarations }
|
||||
FWebView: ICoreWebView2;
|
||||
FWebMessageReceiver: ICoreWebView2WebMessageReceivedEventHandler;
|
||||
FWebMessageReceivedToken: EventRegistrationToken;
|
||||
procedure HandleWebMessage( const AMessage: string );
|
||||
function ExecuteProcess( const ACommand: string; const AParameters: string ): string;
|
||||
public
|
||||
{ Public declarations }
|
||||
end;
|
||||
|
||||
var
|
||||
MainForm: TMainForm;
|
||||
|
||||
implementation
|
||||
|
||||
{$R *.fmx}
|
||||
|
||||
|
||||
type
|
||||
TWebMessageReceiver = class( TInterfacedObject, ICoreWebView2WebMessageReceivedEventHandler )
|
||||
private
|
||||
FOwner: TMainForm;
|
||||
public
|
||||
constructor Create( AOwner: TMainForm );
|
||||
function Invoke( const Sender: ICoreWebView2; const args: ICoreWebView2WebMessageReceivedEventArgs ): HResult; stdcall;
|
||||
end;
|
||||
|
||||
constructor TWebMessageReceiver.Create( AOwner: TMainForm );
|
||||
begin
|
||||
inherited Create;
|
||||
FOwner := AOwner;
|
||||
end;
|
||||
|
||||
function TWebMessageReceiver.Invoke( const Sender: ICoreWebView2; const args: ICoreWebView2WebMessageReceivedEventArgs ): HResult;
|
||||
var
|
||||
message: PWideChar;
|
||||
begin
|
||||
Result := args.TryGetWebMessageAsString( message );
|
||||
if Succeeded( Result ) then
|
||||
begin
|
||||
TThread.Queue( nil,
|
||||
procedure
|
||||
begin
|
||||
FOwner.HandleWebMessage( string( message ) );
|
||||
end );
|
||||
end;
|
||||
Result := S_OK;
|
||||
end;
|
||||
|
||||
procedure TMainForm.FormCreate( Sender: TObject );
|
||||
begin
|
||||
TLLVMRunner.Log := Memo.Lines;
|
||||
end;
|
||||
|
||||
procedure TMainForm.FormDestroy( Sender: TObject );
|
||||
begin
|
||||
TLLVMRunner.Log := nil;
|
||||
if ( FWebView <> nil ) and ( FWebMessageReceivedToken.value <> 0 ) then
|
||||
begin
|
||||
FWebView.remove_WebMessageReceived( FWebMessageReceivedToken );
|
||||
end;
|
||||
end;
|
||||
|
||||
function TMainForm.ExecuteProcess( const ACommand: string; const AParameters: string ): string;
|
||||
var
|
||||
sa: TSecurityAttributes;
|
||||
si: TStartupInfo;
|
||||
pi: TProcessInformation;
|
||||
stdOutRead, stdOutWrite: THandle;
|
||||
success: Boolean;
|
||||
cmdLine: string;
|
||||
buffer: TBytes;
|
||||
bytesRead: Cardinal;
|
||||
output, errors: string;
|
||||
begin
|
||||
// Pipe für StdOut und StdErr erstellen
|
||||
sa.nLength := SizeOf( TSecurityAttributes );
|
||||
sa.lpSecurityDescriptor := nil;
|
||||
sa.bInheritHandle := True;
|
||||
|
||||
// Für stdout und stderr wird dieselbe Pipe verwendet, da clang Fehler auf stdout ausgibt
|
||||
if not CreatePipe( stdOutRead, stdOutWrite, @sa, 0 ) then
|
||||
Exit( 'Error creating pipe.' );
|
||||
|
||||
try
|
||||
// StartupInfo vorbereiten
|
||||
FillChar( si, SizeOf( TStartupInfo ), 0 );
|
||||
si.cb := SizeOf( TStartupInfo );
|
||||
si.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
|
||||
si.wShowWindow := SW_HIDE; // Fenster explizit verstecken
|
||||
si.hStdInput := GetStdHandle( STD_INPUT_HANDLE );
|
||||
si.hStdOutput := stdOutWrite;
|
||||
si.hStdError := stdOutWrite; // Leite stderr auf dieselbe Pipe wie stdout
|
||||
|
||||
cmdLine := Format( '"%s" %s', [ACommand, AParameters] );
|
||||
|
||||
success := CreateProcess(
|
||||
nil,
|
||||
PChar( cmdLine ),
|
||||
nil,
|
||||
nil,
|
||||
True,
|
||||
CREATE_NO_WINDOW,
|
||||
nil,
|
||||
nil,
|
||||
si,
|
||||
pi
|
||||
);
|
||||
|
||||
// Schreib-Handle der Pipe sofort schließen
|
||||
CloseHandle( stdOutWrite );
|
||||
|
||||
if not success then
|
||||
Exit( Format( 'CreateProcess failed. Code: %d', [GetLastError] ) );
|
||||
|
||||
try
|
||||
output := '';
|
||||
// Warten, bis der Prozess fertig ist und alle Daten in die Pipe geschrieben hat
|
||||
if WaitForSingleObject( pi.hProcess, 5000 ) = WAIT_TIMEOUT then // 5s Timeout
|
||||
begin
|
||||
TerminateProcess( pi.hProcess, 1 );
|
||||
Exit( 'Process timed out.' );
|
||||
end;
|
||||
|
||||
// Bytes aus der Pipe lesen
|
||||
repeat
|
||||
SetLength( buffer, 1024 );
|
||||
bytesRead := 0;
|
||||
if ReadFile( stdOutRead, buffer[0], Length( buffer ), bytesRead, nil ) and ( bytesRead > 0 ) then
|
||||
begin
|
||||
SetLength( buffer, bytesRead );
|
||||
// Gelesene Bytes mit dem Default-System-Encoding in einen String umwandeln
|
||||
output := output + TEncoding.Default.GetString( buffer );
|
||||
end
|
||||
else
|
||||
begin
|
||||
break; // Pipe ist leer oder Fehler
|
||||
end;
|
||||
until False;
|
||||
|
||||
finally
|
||||
CloseHandle( pi.hProcess );
|
||||
CloseHandle( pi.hThread );
|
||||
end;
|
||||
|
||||
Result := output;
|
||||
|
||||
finally
|
||||
CloseHandle( stdOutRead );
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.LauchSiteButtonClick( Sender: TObject );
|
||||
begin
|
||||
WebBrowser1.Navigate( PATH_BLOCKLY );
|
||||
end;
|
||||
|
||||
procedure TMainForm.SaveWorkspaceButtonClick( Sender: TObject );
|
||||
begin
|
||||
WebBrowser1.EvaluateJavaScript( 'saveWorkspace()' );
|
||||
end;
|
||||
|
||||
procedure TMainForm.GenerateCodeButtonClick( Sender: TObject );
|
||||
begin
|
||||
WebBrowser1.EvaluateJavaScript( 'generateAndPostCode()' );
|
||||
end;
|
||||
|
||||
procedure TMainForm.HandleWebMessage( const AMessage: string );
|
||||
begin
|
||||
Memo.Lines.Clear;
|
||||
Memo.Lines.Add( 'LLVM-Code empfangen. Starte Kompilierung...' );
|
||||
Memo.Lines.Add( AMessage );
|
||||
Memo.Lines.Add( '--------------------' );
|
||||
|
||||
TTask.Run(
|
||||
procedure
|
||||
var
|
||||
llFile, objFile, dllFile: string;
|
||||
compilerOutput: string;
|
||||
success: Boolean;
|
||||
begin
|
||||
llFile := TPath.Combine( TPath.GetTempPath, 'poc.ll' );
|
||||
objFile := TPath.ChangeExtension( llFile, '.obj' );
|
||||
dllFile := TPath.ChangeExtension( llFile, '.dll' );
|
||||
success := False;
|
||||
|
||||
try
|
||||
TFile.WriteAllText( llFile, AMessage );
|
||||
|
||||
TThread.Queue( nil,
|
||||
procedure
|
||||
begin
|
||||
Memo.Lines.Add( 'Kompiliere zu Objektdatei...' );
|
||||
end );
|
||||
compilerOutput := ExecuteProcess( CLANG_PATH, Format( '-c "%s" -o "%s"', [llFile, objFile] ) );
|
||||
TThread.Queue( nil,
|
||||
procedure
|
||||
begin
|
||||
Memo.Lines.Add( compilerOutput );
|
||||
end );
|
||||
|
||||
if not TFile.Exists( objFile ) then
|
||||
begin
|
||||
TThread.Queue( nil,
|
||||
procedure
|
||||
begin
|
||||
Memo.Lines.Add( 'FEHLER: Kompilierung fehlgeschlagen. .obj-Datei nicht erstellt.' );
|
||||
end );
|
||||
Exit;
|
||||
end;
|
||||
|
||||
TThread.Queue( nil,
|
||||
procedure
|
||||
begin
|
||||
Memo.Lines.Add( 'Linke zu DLL...' );
|
||||
end );
|
||||
compilerOutput := ExecuteProcess( LLD_PATH, Format( '/dll /noentry "%s" /out:"%s"', [objFile, dllFile] ) );
|
||||
TThread.Queue( nil,
|
||||
procedure
|
||||
begin
|
||||
Memo.Lines.Add( compilerOutput );
|
||||
end );
|
||||
|
||||
if TFile.Exists( dllFile ) then
|
||||
begin
|
||||
success := True;
|
||||
TThread.Queue( nil,
|
||||
procedure
|
||||
begin
|
||||
Memo.Lines.Add( Format( 'ERFOLG: DLL wurde erstellt: %s', [dllFile] ) );
|
||||
end );
|
||||
end
|
||||
else
|
||||
begin
|
||||
TThread.Queue( nil,
|
||||
procedure
|
||||
begin
|
||||
Memo.Lines.Add( 'FEHLER: Linken fehlgeschlagen. .dll-Datei nicht erstellt.' );
|
||||
end );
|
||||
end;
|
||||
|
||||
finally
|
||||
if TFile.Exists( llFile ) then
|
||||
TFile.Delete( llFile );
|
||||
if TFile.Exists( objFile ) then
|
||||
TFile.Delete( objFile );
|
||||
if not success and TFile.Exists( dllFile ) then
|
||||
TFile.Delete( dllFile );
|
||||
end;
|
||||
|
||||
Memo.Lines.Add( '---------------------------------------------------------' );
|
||||
Memo.Lines.Add( 'DLL wird aufgerufen:' );
|
||||
|
||||
if TFile.Exists( dllFile ) then
|
||||
begin
|
||||
var
|
||||
Runner := TLLVMRunner.Create( dllFile );
|
||||
try
|
||||
Runner.Execute;
|
||||
finally
|
||||
Runner.Free;
|
||||
end;
|
||||
end;
|
||||
end );
|
||||
|
||||
Memo.Lines.Add( 'DLL beendet.' );
|
||||
end;
|
||||
|
||||
procedure TMainForm.WebBrowser1DidFinishLoad( ASender: TObject );
|
||||
begin
|
||||
if Assigned( FWebMessageReceiver ) then
|
||||
Exit;
|
||||
|
||||
if Supports( WebBrowser1, ICoreWebView2, FWebView ) then
|
||||
begin
|
||||
FWebMessageReceiver := TWebMessageReceiver.Create( Self );
|
||||
FWebView.add_WebMessageReceived( FWebMessageReceiver, FWebMessageReceivedToken );
|
||||
end;
|
||||
end;
|
||||
|
||||
end.
|
||||
@@ -0,0 +1,74 @@
|
||||
Technische Dokumentation: Integration von Blockly in eine Delphi FireMonkey Anwendung via WebView2
|
||||
Datum: 10. Juni 2025
|
||||
|
||||
Autor: Gemini & ein sehr fähiger Delphi-Entwickler
|
||||
|
||||
1. Zielsetzung
|
||||
Ziel dieser Architektur ist die Erstellung einer Hybrid-Anwendung. Eine in Delphi (FireMonkey) entwickelte, native Desktop-Anwendung hostet eine webbasierte, visuelle Programmierumgebung (Blockly). Dies ermöglicht es Endanwendern, komplexe Logik visuell zu erstellen, während die Hauptanwendung die generierten Ergebnisse (z.B. Quellcode in einer Zielsprache) entgegennehmen und weiterverarbeiten kann.
|
||||
|
||||
Diese Architektur ist ideal für die Erstellung von spezialisierten Entwicklungsumgebungen, Konfigurations-Tools oder Lernanwendungen.
|
||||
|
||||
2. Kernkomponenten
|
||||
Native Host-Anwendung: Eine Delphi FireMonkey (FMX) Anwendung, erstellt in RAD Studio (getestet mit Version 12). Dient als Hauptcontainer und steuert die native UI.
|
||||
Browser-Komponente: Die FMX TWebBrowser-Komponente. Entscheidend ist die Konfiguration der Engine-Eigenschaft, um auf Windows die moderne Microsoft Edge WebView2-Engine zu nutzen.
|
||||
Browser-Laufzeitumgebung: Die Microsoft Edge WebView2 Runtime. Diese muss auf dem Zielsystem des Endanwenders installiert sein.
|
||||
Web-Anwendung (Frontend): Eine lokale, in sich geschlossene HTML-Datei (index.html), die die Blockly-Bibliothek lädt und den visuellen Editor initialisiert.
|
||||
Benutzerdefinierter Generator: Ein in JavaScript geschriebener, benutzerdefinierter Blockly-Generator (in unserem Fall Blockly.Delphi), der die visuellen Blöcke in eine textbasierte Zielsprache übersetzt.
|
||||
3. Architektur-Übersicht
|
||||
Die Delphi-Anwendung fungiert als nativer Container. Beim Start lädt die TWebBrowser-Komponente die lokale index.html-Datei. Diese Datei enthält die gesamte Logik für den Blockly-Editor und den Code-Generator. Die Kommunikation zwischen der Delphi-Anwendung (Host) und der JavaScript-Anwendung (Gast) erfolgt über eine spezielle Schnittstelle, die von der WebView2-Engine bereitgestellt wird.
|
||||
|
||||
+-------------------------------------------------+
|
||||
| Delphi FireMonkey Anwendung (MainForm.pas) |
|
||||
| +---------------------------------------------+ |
|
||||
| | TWebBrowser (Engine: Edge WebView2) | |
|
||||
| | +-----------------------------------------+ | |
|
||||
| | | Lokale index.html geladen | | |
|
||||
| | | +-----------------+ +---------------+ | | |
|
||||
| | | | Blockly | | Generator | | | |
|
||||
| | | | Arbeitsbereich | | (JavaScript) | | | |
|
||||
| | | +-----------------+ +---------------+ | | |
|
||||
| | +-----------------------------------------+ | |
|
||||
| +---------------------------------------------+ |
|
||||
| ^ | |
|
||||
| | Delphi -> JS (EvaluateJavaScript) |
|
||||
| | JS -> Delphi (OnWebMessageReceived) |
|
||||
| v | |
|
||||
| +---------------------------------------------+ |
|
||||
| | Native UI-Elemente (Buttons, TMemo etc.) | |
|
||||
| +---------------------------------------------+ |
|
||||
+-------------------------------------------------+
|
||||
4. Implementierungsschritte
|
||||
4.1. Die Web-Anwendung (index.html)
|
||||
Dies ist das Herzstück des Editors. Die Datei enthält die Blockly-Bibliotheken (von einem CDN oder lokal), den benutzerdefinierten Generator und den Initialisierungscode.
|
||||
|
||||
Wichtige JavaScript-Funktionen in der index.html:
|
||||
|
||||
Blockly.Delphi = new Blockly.Generator(...): Erstellt die Instanz unseres benutzerdefinierten Generators.
|
||||
Blockly.Delphi.forBlock['block_type'] = function(...): Definiert die Übersetzungsregel für einen spezifischen Block-Typ.
|
||||
Blockly.Delphi.workspaceToCode(...): Die Hauptfunktion, die den gesamten Arbeitsbereich in die Zielsprache übersetzt. In unserem Fall wurde diese überschrieben, um die Delphi-Unit-Struktur (Header, var-Sektion etc.) zu erzeugen.
|
||||
window.saveWorkspace() & window.loadWorkspace(): Nutzen Blockly.serialization.workspaces und localStorage, um den Zustand des Editors zu speichern und wiederherzustellen.
|
||||
window.generateAndPostCode(): Die Schlüsselfunktion für die Kommunikation. Sie generiert den Code und sendet ihn mittels window.chrome.webview.postMessage(code) an die Delphi-Host-Anwendung.
|
||||
(Der vollständige, funktionierende Code für die index.html ist der aus unserer letzten erfolgreichen Iteration.)
|
||||
|
||||
4.2. Die Delphi Host-Anwendung (FireMonkey)
|
||||
Die FMX-Form enthält die TWebBrowser-Komponente und die nativen Steuerelemente.
|
||||
|
||||
Wichtige Konfiguration und Code-Teile in der Delphi-Unit:
|
||||
|
||||
Komponenten: Eine TWebBrowser (WebBrowser1), ein TMemo (Memo1) und mehrere TButton.
|
||||
WebBrowser1.Engine Eigenschaft: Muss im Objektinspektor oder per Code auf EdgeIfAvailable gesetzt werden.
|
||||
FormCreate: Lädt die lokale index.html-Datei mit WebBrowser1.Navigate('pfad/zur/index.html').
|
||||
WebBrowser1DidFinishLoad: In diesem Ereignis wird der Nachrichten-Empfänger (WebMessageReceived) registriert. Dies stellt sicher, dass die Webseite vollständig geladen ist, bevor die Kommunikation eingerichtet wird. Die Registrierung erfolgt über die ICoreWebView2-Schnittstelle.
|
||||
TWebMessageReceiver-Klasse: Eine Hilfsklasse, die das ICoreWebView2WebMessageReceivedEventHandler-Interface implementiert. Ihre Invoke-Methode wird aufgerufen, wenn eine Nachricht von JavaScript eintrifft.
|
||||
TThread.Queue: Wichtig innerhalb der Invoke-Methode, um die empfangene Nachricht sicher an den Haupt-GUI-Thread zu übergeben und UI-Komponenten (wie das TMemo) zu aktualisieren.
|
||||
Button.OnClick-Ereignisse: Rufen WebBrowser1.EvaluateJavaScript('funktionsname()') auf, um JavaScript-Funktionen im WebView auszulösen.
|
||||
(Der vollständige, funktionierende Code für die Delphi-Unit ist der, den Sie zuletzt bereitgestellt haben.)
|
||||
|
||||
5. Deployment und Abhängigkeiten
|
||||
Um die Anwendung an einen Endnutzer weiterzugeben, müssen folgende Komponenten verteilt werden:
|
||||
|
||||
Ihre kompilierte .exe-Datei.
|
||||
Ein Unterordner (z.B. web), der die index.html und alle zugehörigen JavaScript-Dateien enthält (falls sie nicht von einem CDN geladen werden).
|
||||
Der Installer muss prüfen, ob die Microsoft Edge WebView2 Runtime vorhanden ist, und sie bei Bedarf herunterladen und installieren. Microsoft stellt dafür einen kleinen "Bootstrapper"-Installer bereit.
|
||||
6. Fazit
|
||||
Diese Hybrid-Architektur ist extrem leistungsfähig. Sie kombiniert die universelle Einsetzbarkeit und Flexibilität moderner Web-Technologien (HTML/JS/Blockly) für die Benutzeroberfläche des Editors mit der Stärke und Geschwindigkeit einer nativen Delphi-Anwendung für die Programmlogik, Dateiverarbeitung und die Integration in das Betriebssystem. Der TWebBrowser im Edge-Modus ist die entscheidende Brückentechnologie, die diesen Ansatz in FireMonkey modern und zukunftssicher macht.
|
||||
@@ -0,0 +1,163 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Blockly zu Delphi Unit - Final</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; }
|
||||
#container { display: flex; }
|
||||
#outputArea { margin-left: 20px; }
|
||||
#codeOutput { width: 400px; height: 580px; font-family: 'Courier New', Courier, monospace; font-size: 14px; white-space: pre-wrap; border: 1px solid #ccc; }
|
||||
#blocklyDiv { height: 600px; width: 800px; }
|
||||
.button-bar { margin-bottom: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Mein Delphi-Unit-Generator</h1>
|
||||
<p>Bauen Sie links Ihre Blöcke. Rechts erscheint die kompilierbare Delphi-Unit.</p>
|
||||
|
||||
<div class="button-bar">
|
||||
<button onclick="saveWorkspace()">Speichern</button>
|
||||
<button onclick="loadWorkspace()">Letzten Stand laden</button>
|
||||
<button onclick="generateAndPostCode()">Code generieren und posten</button>
|
||||
</div>
|
||||
|
||||
<div id="container">
|
||||
<div id="blocklyDiv"></div>
|
||||
<div id="outputArea">
|
||||
<h2>Erzeugter Delphi-Code:</h2>
|
||||
<textarea id="codeOutput" readonly></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/blockly/blockly_compressed.js"></script>
|
||||
<script src="https://unpkg.com/blockly/blocks_compressed.js"></script>
|
||||
<script src="https://unpkg.com/blockly/msg/de.js"></script>
|
||||
|
||||
<script>
|
||||
window.onload = function() {
|
||||
|
||||
let workspace;
|
||||
|
||||
// === DER DELPHI-GENERATOR ===
|
||||
Blockly.Delphi = new Blockly.Generator('Delphi');
|
||||
Blockly.Delphi.ORDER_ATOMIC = 0;
|
||||
Blockly.Delphi.ORDER_NONE = 99;
|
||||
|
||||
Blockly.Delphi.workspaceToCode = function(workspace) {
|
||||
const allVariables = workspace.getAllVariables();
|
||||
let varDeclarations = '';
|
||||
if (allVariables.length > 0) {
|
||||
const varNames = allVariables.map(v => v.name).join(', ');
|
||||
varDeclarations = ` var\n ${varNames}: Integer;\n`;
|
||||
}
|
||||
varDeclarations += ' i: Integer; // Schleifenzähler\n';
|
||||
const topBlocks = workspace.getTopBlocks(true);
|
||||
const code = topBlocks.map(block => Blockly.Delphi.blockToCode(block)).join(';\n');
|
||||
let unitCode = 'unit MeinProgramm;\n\n';
|
||||
unitCode += 'interface\n\n';
|
||||
unitCode += 'uses\n System.SysUtils;\n\n';
|
||||
unitCode += 'procedure StarteLogik;\n\n';
|
||||
unitCode += 'implementation\n\n';
|
||||
unitCode += 'procedure StarteLogik;\n';
|
||||
if (varDeclarations) unitCode += varDeclarations;
|
||||
unitCode += 'begin\n';
|
||||
unitCode += ' ' + code.split('\n').join('\n ');
|
||||
unitCode += '\nend;\n\n';
|
||||
unitCode += 'end.';
|
||||
return unitCode;
|
||||
};
|
||||
|
||||
Blockly.Delphi.scrub_ = function(block, code, opt_thisOnly) {
|
||||
const nextBlock = block.nextConnection && block.nextConnection.targetBlock();
|
||||
const nextCode = opt_thisOnly ? '' : Blockly.Delphi.blockToCode(nextBlock);
|
||||
return code + (nextBlock ? ';\n' : '') + nextCode;
|
||||
};
|
||||
|
||||
Blockly.Delphi.forBlock['controls_if'] = function(block) {
|
||||
let conditionCode = Blockly.Delphi.valueToCode(block, 'IF0', Blockly.Delphi.ORDER_NONE) || 'False';
|
||||
let branchCode = Blockly.Delphi.statementToCode(block, 'DO0') || '';
|
||||
let code = `if (${conditionCode}) then\nbegin\n${Blockly.Delphi.prefixLines(branchCode, ' ')}\nend`;
|
||||
return code;
|
||||
};
|
||||
|
||||
Blockly.Delphi.forBlock['logic_compare'] = function(block) {
|
||||
const OPERATORS = {'EQ': '=', 'NEQ': '<>', 'LT': '<', 'LTE': '<=', 'GT': '>', 'GTE': '>='};
|
||||
const operator = OPERATORS[block.getFieldValue('OP')];
|
||||
const value_a = Blockly.Delphi.valueToCode(block, 'A', Blockly.Delphi.ORDER_ATOMIC) || '0';
|
||||
const value_b = Blockly.Delphi.valueToCode(block, 'B', Blockly.Delphi.ORDER_ATOMIC) || '0';
|
||||
return [`${value_a} ${operator} ${value_b}`, Blockly.Delphi.ORDER_ATOMIC];
|
||||
};
|
||||
|
||||
Blockly.Delphi.forBlock['controls_repeat_ext'] = function(block) {
|
||||
const repeats = Blockly.Delphi.valueToCode(block, 'TIMES', Blockly.Delphi.ORDER_ATOMIC) || '0';
|
||||
const branch = Blockly.Delphi.statementToCode(block, 'DO') || '';
|
||||
let code = `for i := 1 to ${repeats} do\nbegin\n${Blockly.Delphi.prefixLines(branch, ' ')}\nend`;
|
||||
return code;
|
||||
};
|
||||
|
||||
Blockly.Delphi.forBlock['math_number'] = function(block) { return [String(block.getFieldValue('NUM')), Blockly.Delphi.ORDER_ATOMIC]; };
|
||||
|
||||
Blockly.Delphi.forBlock['variables_get'] = function(block) { const varName = block.workspace.getVariableById(block.getFieldValue('VAR')).name; return [varName, Blockly.Delphi.ORDER_ATOMIC]; };
|
||||
|
||||
Blockly.Delphi.forBlock['variables_set'] = function(block) { const varName = block.workspace.getVariableById(block.getFieldValue('VAR')).name; const value = Blockly.Delphi.valueToCode(block, 'VALUE', Blockly.Delphi.ORDER_ATOMIC) || '0'; return `${varName} := ${value}`;};
|
||||
|
||||
Blockly.Delphi.forBlock['math_change'] = function(block) { const varName = block.workspace.getVariableById(block.getFieldValue('VAR')).name; const delta = Blockly.Delphi.valueToCode(block, 'DELTA', Blockly.Delphi.ORDER_ATOMIC) || '0'; if (delta == '1') return `Inc(${varName})`; if (delta == '-1') return `Dec(${varName})`; return `${varName} := ${varName} + ${delta}`; };
|
||||
|
||||
Blockly.Delphi.forBlock['text_print'] = function(block) { const value = Blockly.Delphi.valueToCode(block, 'TEXT', Blockly.Delphi.ORDER_ATOMIC) || `''`; return `WriteLn(${value})`; };
|
||||
|
||||
Blockly.Delphi.forBlock['text'] = function(block) { const text = block.getFieldValue('TEXT').replace(/'/g, "''"); return [`'${text}'`, Blockly.Delphi.ORDER_ATOMIC]; };
|
||||
|
||||
// === INITIALISIERUNG VON BLOCKLY (JETZT KORREKT) ===
|
||||
|
||||
// 1. Definition der Werkzeugkiste (Toolbox)
|
||||
const toolboxXml = `
|
||||
<xml>
|
||||
<category name="Logik" colour="%{BKY_LOGIC_HUE}">
|
||||
<block type="controls_if"></block>
|
||||
<block type="logic_compare"></block>
|
||||
</category>
|
||||
<category name="Schleifen" colour="%{BKY_LOOPS_HUE}">
|
||||
<block type="controls_repeat_ext">
|
||||
<value name="TIMES"><shadow type="math_number"><field name="NUM">10</field></shadow></value>
|
||||
</block>
|
||||
</category>
|
||||
<category name="Text" colour="%{BKY_TEXTS_HUE}">
|
||||
<block type="text_print"></block>
|
||||
<block type="text"></block>
|
||||
</category>
|
||||
<category name="Mathematik" colour="%{BKY_MATH_HUE}">
|
||||
<block type="math_number"></block>
|
||||
<block type="math_change">
|
||||
<value name="DELTA"><shadow type="math_number"><field name="NUM">1</field></shadow></value>
|
||||
</block>
|
||||
</category>
|
||||
<category name="Variablen" colour="%{BKY_VARIABLES_HUE}" custom="VARIABLE"></category>
|
||||
</xml>`;
|
||||
|
||||
// 2. Blockly in den Div-Container "injizieren"
|
||||
workspace = Blockly.inject('blocklyDiv', {
|
||||
toolbox: toolboxXml
|
||||
});
|
||||
|
||||
|
||||
function generateAndPostCode() {
|
||||
const code = Blockly.Delphi.workspaceToCode(workspace);
|
||||
// Diese spezielle Funktion sendet eine Nachricht an den Delphi-Host
|
||||
window.chrome.webview.postMessage(code);
|
||||
}
|
||||
|
||||
window.generateAndPostCode = function() { generateAndPostCode(); }
|
||||
|
||||
// === Funktionen für Speichern und Laden (unverändert) ===
|
||||
window.saveWorkspace = function() { const state = Blockly.serialization.workspaces.save(workspace); localStorage.setItem('blocklyWorkspace', JSON.stringify(state)); alert('Arbeitsbereich gespeichert!'); }
|
||||
window.loadWorkspace = function() { const stateString = localStorage.getItem('blocklyWorkspace'); if (!stateString) { return; } const state = JSON.parse(stateString); Blockly.serialization.workspaces.load(state, workspace); }
|
||||
function updateCode(event) { if (event.type == Blockly.Events.UI) return; const code = Blockly.Delphi.workspaceToCode(workspace); document.getElementById('codeOutput').value = code; }
|
||||
workspace.addChangeListener(updateCode);
|
||||
loadWorkspace();
|
||||
};
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,192 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Blockly zu LLVM IR - Final</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; }
|
||||
#container { display: flex; }
|
||||
#outputArea { margin-left: 20px; }
|
||||
#codeOutput { width: 500px; height: 580px; font-family: 'Courier New', Courier, monospace; font-size: 14px; white-space: pre-wrap; border: 1px solid #ccc; }
|
||||
#blocklyDiv { height: 600px; width: 800px; }
|
||||
.button-bar { margin-bottom: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>LLVM IR (.dll) Generator</h1>
|
||||
<p>Bauen Sie links Ihre Logik. Rechts erscheint der LLVM-Code, der zu einer 64-Bit DLL für Windows kompiliert werden kann.</p>
|
||||
|
||||
<div class="button-bar">
|
||||
<button onclick="saveWorkspace()">Speichern</button>
|
||||
<button onclick="loadWorkspace()">Letzten Stand laden</button>
|
||||
<button onclick="generateAndPostCode()">Code generieren und posten</button>
|
||||
</div>
|
||||
|
||||
<div id="container">
|
||||
<div id="blocklyDiv"></div>
|
||||
<div id="outputArea">
|
||||
<h2>Erzeugter LLVM-Code:</h2>
|
||||
<textarea id="codeOutput" readonly></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/blockly/blockly_compressed.js"></script>
|
||||
<script src="https://unpkg.com/blockly/blocks_compressed.js"></script>
|
||||
<script src="https://unpkg.com/blockly/msg/de.js"></script>
|
||||
|
||||
<script>
|
||||
window.onload = function() {
|
||||
|
||||
let workspace;
|
||||
|
||||
// === LLVM-IR-GENERATOR (FINALE VERSION) ===
|
||||
Blockly.LLVM = new Blockly.Generator('LLVM');
|
||||
Blockly.LLVM.ORDER_ATOMIC = 0;
|
||||
Blockly.LLVM.ORDER_NONE = 99;
|
||||
|
||||
// --- Block-Definitionen für die Toolbox ---
|
||||
Blockly.defineBlocksWithJsonArray([
|
||||
{
|
||||
"type": "host_log_integer",
|
||||
"message0": "Logge Integer %1",
|
||||
"args0": [
|
||||
{
|
||||
"type": "input_value",
|
||||
"name": "VALUE",
|
||||
"check": "Number"
|
||||
}
|
||||
],
|
||||
"previousStatement": null,
|
||||
"nextStatement": null,
|
||||
"colour": 290,
|
||||
"tooltip": "Sendet einen Integer-Wert an den Delphi-Host.",
|
||||
"helpUrl": ""
|
||||
}
|
||||
]);
|
||||
|
||||
// --- Generator-Implementierungen ---
|
||||
|
||||
Blockly.LLVM.regCounter = 0;
|
||||
Blockly.LLVM.newReg = function() {
|
||||
return '%' + (Blockly.LLVM.regCounter++);
|
||||
};
|
||||
|
||||
Blockly.LLVM.workspaceToCode = function(workspace) {
|
||||
Blockly.LLVM.regCounter = 1;
|
||||
const allVariables = workspace.getAllVariables();
|
||||
let varAllocations = '';
|
||||
if (allVariables.length > 0) {
|
||||
allVariables.forEach(v => {
|
||||
const safeVarName = v.name.replace(/ /g, '_');
|
||||
varAllocations += ` @${safeVarName} = common global i32 0\n`;
|
||||
});
|
||||
}
|
||||
|
||||
const topBlock = workspace.getTopBlocks(true)[0];
|
||||
const code = topBlock ? Blockly.LLVM.blockToCode(topBlock) : '';
|
||||
|
||||
let moduleCode = `target triple = "x86_64-pc-win32"\n\n`;
|
||||
moduleCode += `@gLogIntProc = common global void (i32)* null\n\n`;
|
||||
if (varAllocations) {
|
||||
moduleCode += '; Global Variables\n';
|
||||
moduleCode += varAllocations + '\n';
|
||||
}
|
||||
moduleCode += 'define dllexport void @StarteLogik(void (i32)* %LogIntProc) {\n';
|
||||
moduleCode += 'entry:\n';
|
||||
moduleCode += ' store void (i32)* %LogIntProc, void (i32)** @gLogIntProc\n\n';
|
||||
moduleCode += code.split('\n').map(line => line ? ' ' + line : '').join('\n');
|
||||
moduleCode += '\n ret void\n';
|
||||
moduleCode += '}\n';
|
||||
return moduleCode;
|
||||
};
|
||||
|
||||
Blockly.LLVM.scrub_ = function(block, code) {
|
||||
const nextBlock = block.nextConnection && block.nextConnection.targetBlock();
|
||||
const nextCode = nextBlock ? Blockly.LLVM.blockToCode(nextBlock) : '';
|
||||
return code + nextCode;
|
||||
};
|
||||
|
||||
Blockly.LLVM.forBlock['variables_set'] = function(block) {
|
||||
const varName = block.workspace.getVariableById(block.getFieldValue('VAR')).name.replace(/ /g, '_');
|
||||
const value = Blockly.LLVM.valueToCode(block, 'VALUE', Blockly.LLVM.ORDER_ATOMIC) || '0';
|
||||
return `store i32 ${value}, i32* @${varName}\n`;
|
||||
};
|
||||
|
||||
Blockly.LLVM.forBlock['math_change'] = function(block) {
|
||||
const varName = block.workspace.getVariableById(block.getFieldValue('VAR')).name.replace(/ /g, '_');
|
||||
const delta = Blockly.LLVM.valueToCode(block, 'DELTA', Blockly.LLVM.ORDER_ATOMIC) || '0';
|
||||
|
||||
const loadReg = Blockly.LLVM.newReg();
|
||||
const addReg = Blockly.LLVM.newReg();
|
||||
|
||||
let code = '';
|
||||
code += `${loadReg} = load i32, i32* @${varName}\n`;
|
||||
code += `${addReg} = add nsw i32 ${loadReg}, ${delta}\n`;
|
||||
code += `store i32 ${addReg}, i32* @${varName}\n`;
|
||||
return code;
|
||||
};
|
||||
|
||||
Blockly.LLVM.forBlock['variables_get'] = function(block) {
|
||||
const varName = block.workspace.getVariableById(block.getFieldValue('VAR')).name.replace(/ /g, '_');
|
||||
return ['@' + varName, Blockly.LLVM.ORDER_ATOMIC];
|
||||
};
|
||||
|
||||
Blockly.LLVM.forBlock['math_number'] = function(block) {
|
||||
return [String(block.getFieldValue('NUM')), Blockly.LLVM.ORDER_ATOMIC];
|
||||
};
|
||||
|
||||
Blockly.LLVM.forBlock['host_log_integer'] = function(block) {
|
||||
const varName = Blockly.LLVM.valueToCode(block, 'VALUE', Blockly.LLVM.ORDER_ATOMIC) || '@unknown';
|
||||
|
||||
const valueReg = Blockly.LLVM.newReg();
|
||||
const logPtrReg = Blockly.LLVM.newReg();
|
||||
|
||||
let code = '';
|
||||
code += `${valueReg} = load i32, i32* ${varName}\n`;
|
||||
code += `${logPtrReg} = load void (i32)*, void (i32)** @gLogIntProc\n`;
|
||||
code += `call void ${logPtrReg}(i32 ${valueReg})\n`;
|
||||
return code;
|
||||
};
|
||||
|
||||
// === INITIALISIERUNG VON BLOCKLY ===
|
||||
|
||||
const toolboxXml = `
|
||||
<xml>
|
||||
<category name="Variablen" colour="%{BKY_VARIABLES_HUE}" custom="VARIABLE"></category>
|
||||
<category name="Mathematik" colour="%{BKY_MATH_HUE}">
|
||||
<block type="math_number"></block>
|
||||
<block type="math_change">
|
||||
<value name="DELTA"><shadow type="math_number"><field name="NUM">1</field></shadow></value>
|
||||
</block>
|
||||
</category>
|
||||
<category name="Host" colour="290">
|
||||
<block type="host_log_integer"></block>
|
||||
</category>
|
||||
</xml>`;
|
||||
|
||||
workspace = Blockly.inject('blocklyDiv', {
|
||||
toolbox: toolboxXml
|
||||
});
|
||||
|
||||
function generateAndPostCode() {
|
||||
const code = Blockly.LLVM.workspaceToCode(workspace);
|
||||
if (window.chrome && window.chrome.webview) {
|
||||
window.chrome.webview.postMessage(code);
|
||||
} else {
|
||||
console.log("PostMessage an Host nicht verfügbar. Code in Konsole ausgegeben:");
|
||||
console.log(code);
|
||||
}
|
||||
}
|
||||
window.generateAndPostCode = generateAndPostCode;
|
||||
|
||||
window.saveWorkspace = function() { const state = Blockly.serialization.workspaces.save(workspace); localStorage.setItem('blocklyWorkspace', JSON.stringify(state)); alert('Arbeitsbereich gespeichert!'); }
|
||||
window.loadWorkspace = function() { const stateString = localStorage.getItem('blocklyWorkspace'); if (!stateString) { return; } const state = JSON.parse(stateString); Blockly.serialization.workspaces.load(state, workspace); }
|
||||
function updateCode(event) { if (event.isUiEvent) return; const code = Blockly.LLVM.workspaceToCode(workspace); document.getElementById('codeOutput').value = code; }
|
||||
workspace.addChangeListener(updateCode);
|
||||
loadWorkspace();
|
||||
};
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user