feat: Add WebView2 backend integration for React UI
- Add WebView2 support to STDLabelAttach project - Create MachineBridge.cs for JavaScript-C# communication - GetConfig/SaveConfig using SETTING.Data reflection - Recipe management (CRUD operations) - IO control interface - Motion control simulation - Add WebSocketServer.cs for HMR development support - Update fWebView.cs with PLC simulation and status updates - Integrate with existing project settings (Data/Setting.json) - Add virtual host mapping (http://hmi.local → FrontEnd/dist) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,23 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Microsoft.Web.WebView2.WinForms;
|
||||
using Newtonsoft.Json;
|
||||
using Project.WebUI;
|
||||
|
||||
namespace Project.Dialog
|
||||
{
|
||||
public partial class fWebView : Form
|
||||
{
|
||||
private WebView2 webView2;
|
||||
private TextBox txtUrl;
|
||||
private Button btnGo;
|
||||
private Button btnBack;
|
||||
private Button btnForward;
|
||||
private Button btnRefresh;
|
||||
private WebView2 webView;
|
||||
private Timer plcTimer;
|
||||
private WebSocketServer _wsServer;
|
||||
|
||||
// Machine State (Simulated PLC Memory)
|
||||
private double currX = 0, currY = 0, currZ = 0;
|
||||
private double targetX = 0, targetY = 0, targetZ = 0;
|
||||
private bool[] inputs = new bool[32];
|
||||
private bool[] outputs = new bool[32];
|
||||
private string systemState = "IDLE";
|
||||
private string currentRecipeId = "1"; // Default recipe
|
||||
|
||||
public fWebView()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitializeWebView();
|
||||
|
||||
// Start WebSocket Server for HMR/Dev
|
||||
try
|
||||
{
|
||||
_wsServer = new WebSocketServer("http://localhost:8081/", this);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("Failed to start WebSocket Server (Port 8081). Run as Admin or allow port.\n" + ex.Message);
|
||||
}
|
||||
|
||||
// Set default inputs (Pressure OK, Estop OK)
|
||||
inputs[4] = true;
|
||||
inputs[6] = true;
|
||||
|
||||
// Load event handler
|
||||
this.Load += FWebView_Load;
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
@@ -26,71 +52,52 @@ namespace Project.Dialog
|
||||
|
||||
// Form
|
||||
this.ClientSize = new System.Drawing.Size(1200, 800);
|
||||
this.Text = "WebView2 Browser";
|
||||
this.Text = "STD Label Attach - Web UI";
|
||||
this.Name = "fWebView";
|
||||
|
||||
// URL TextBox
|
||||
this.txtUrl = new TextBox();
|
||||
this.txtUrl.Location = new System.Drawing.Point(100, 10);
|
||||
this.txtUrl.Size = new System.Drawing.Size(900, 25);
|
||||
this.txtUrl.KeyDown += TxtUrl_KeyDown;
|
||||
|
||||
// Go Button
|
||||
this.btnGo = new Button();
|
||||
this.btnGo.Location = new System.Drawing.Point(1010, 10);
|
||||
this.btnGo.Size = new System.Drawing.Size(70, 25);
|
||||
this.btnGo.Text = "Go";
|
||||
this.btnGo.Click += BtnGo_Click;
|
||||
|
||||
// Back Button
|
||||
this.btnBack = new Button();
|
||||
this.btnBack.Location = new System.Drawing.Point(10, 10);
|
||||
this.btnBack.Size = new System.Drawing.Size(25, 25);
|
||||
this.btnBack.Text = "◀";
|
||||
this.btnBack.Click += BtnBack_Click;
|
||||
|
||||
// Forward Button
|
||||
this.btnForward = new Button();
|
||||
this.btnForward.Location = new System.Drawing.Point(40, 10);
|
||||
this.btnForward.Size = new System.Drawing.Size(25, 25);
|
||||
this.btnForward.Text = "▶";
|
||||
this.btnForward.Click += BtnForward_Click;
|
||||
|
||||
// Refresh Button
|
||||
this.btnRefresh = new Button();
|
||||
this.btnRefresh.Location = new System.Drawing.Point(70, 10);
|
||||
this.btnRefresh.Size = new System.Drawing.Size(25, 25);
|
||||
this.btnRefresh.Text = "⟳";
|
||||
this.btnRefresh.Click += BtnRefresh_Click;
|
||||
|
||||
// WebView2
|
||||
this.webView2 = new WebView2();
|
||||
this.webView2.Location = new System.Drawing.Point(10, 45);
|
||||
this.webView2.Size = new System.Drawing.Size(1170, 740);
|
||||
|
||||
this.Controls.Add(this.txtUrl);
|
||||
this.Controls.Add(this.btnGo);
|
||||
this.Controls.Add(this.btnBack);
|
||||
this.Controls.Add(this.btnForward);
|
||||
this.Controls.Add(this.btnRefresh);
|
||||
this.Controls.Add(this.webView2);
|
||||
this.StartPosition = FormStartPosition.CenterScreen;
|
||||
|
||||
this.ResumeLayout(false);
|
||||
}
|
||||
|
||||
private async void InitializeWebView()
|
||||
{
|
||||
// 1. Setup Virtual Host (http://hmi.local) pointing to FrontEnd/dist folder
|
||||
// Navigate up from bin/debug/ to project root, then to FrontEnd/dist
|
||||
string projectRoot = Path.GetFullPath(Path.Combine(Application.StartupPath, @"..\..\..\.."));
|
||||
string wwwroot = Path.Combine(projectRoot, @"FrontEnd\dist");
|
||||
|
||||
this.Text = $"STD Label Attach - {wwwroot}";
|
||||
|
||||
webView = new WebView2();
|
||||
webView.Dock = DockStyle.Fill;
|
||||
this.Controls.Add(webView);
|
||||
|
||||
try
|
||||
{
|
||||
await webView2.EnsureCoreWebView2Async(null);
|
||||
await webView.EnsureCoreWebView2Async();
|
||||
|
||||
// 네비게이션 이벤트 핸들러
|
||||
webView2.CoreWebView2.NavigationCompleted += CoreWebView2_NavigationCompleted;
|
||||
webView2.CoreWebView2.SourceChanged += CoreWebView2_SourceChanged;
|
||||
if (!Directory.Exists(wwwroot))
|
||||
{
|
||||
MessageBox.Show($"Could not find frontend build at:\n{wwwroot}\n\nPlease run 'npm run build' in the FrontEnd folder.",
|
||||
"Frontend Not Found", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
// Fallback to local wwwroot if needed, or just allow it to fail gracefully
|
||||
Directory.CreateDirectory(wwwroot);
|
||||
}
|
||||
|
||||
// 기본 페이지 로드
|
||||
webView2.CoreWebView2.Navigate("http://localhost:3000");
|
||||
txtUrl.Text = "http://localhost:3000";
|
||||
webView.CoreWebView2.SetVirtualHostNameToFolderMapping(
|
||||
"hmi.local",
|
||||
wwwroot,
|
||||
CoreWebView2HostResourceAccessKind.Allow
|
||||
);
|
||||
|
||||
// 2. Inject Native Object
|
||||
webView.CoreWebView2.AddHostObjectToScript("machine", new MachineBridge(this));
|
||||
|
||||
// 3. Load UI
|
||||
webView.Source = new Uri("http://hmi.local/index.html");
|
||||
|
||||
// Disable default browser keys (F5, F12 etc) if needed for kiosk mode
|
||||
webView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -99,84 +106,99 @@ namespace Project.Dialog
|
||||
}
|
||||
}
|
||||
|
||||
private void CoreWebView2_SourceChanged(object sender, CoreWebView2SourceChangedEventArgs e)
|
||||
// --- Logic Loop ---
|
||||
private void PlcTimer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
txtUrl.Text = webView2.Source.ToString();
|
||||
}
|
||||
// 1. Simulate Motion (Move Current towards Target)
|
||||
currX = Lerp(currX, targetX, 0.1);
|
||||
currY = Lerp(currY, targetY, 0.1);
|
||||
currZ = Lerp(currZ, targetZ, 0.1);
|
||||
|
||||
private void CoreWebView2_NavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
|
||||
{
|
||||
btnBack.Enabled = webView2.CanGoBack;
|
||||
btnForward.Enabled = webView2.CanGoForward;
|
||||
}
|
||||
|
||||
private void BtnGo_Click(object sender, EventArgs e)
|
||||
{
|
||||
NavigateToUrl();
|
||||
}
|
||||
|
||||
private void TxtUrl_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.KeyCode == Keys.Enter)
|
||||
// 2. Prepare Data Packet
|
||||
var payload = new
|
||||
{
|
||||
NavigateToUrl();
|
||||
e.Handled = true;
|
||||
e.SuppressKeyPress = true;
|
||||
}
|
||||
}
|
||||
type = "STATUS_UPDATE",
|
||||
sysState = systemState,
|
||||
position = new { x = currX, y = currY, z = currZ },
|
||||
ioState = GetChangedIOs() // Function to return array of IO states
|
||||
};
|
||||
|
||||
private void NavigateToUrl()
|
||||
{
|
||||
string url = txtUrl.Text;
|
||||
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||
string json = JsonConvert.SerializeObject(payload);
|
||||
|
||||
// 3. Send to React via PostMessage (WebView2)
|
||||
if (webView != null && webView.CoreWebView2 != null)
|
||||
{
|
||||
url = "http://" + url;
|
||||
webView.CoreWebView2.PostWebMessageAsJson(json);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
webView2.CoreWebView2.Navigate(url);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"페이지 로드 실패: {ex.Message}", "Error",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
// 4. Broadcast to WebSocket (Dev/HMR)
|
||||
_wsServer?.Broadcast(json);
|
||||
}
|
||||
|
||||
private void BtnBack_Click(object sender, EventArgs e)
|
||||
private List<object> GetChangedIOs()
|
||||
{
|
||||
if (webView2.CanGoBack)
|
||||
// Simply return list of all active IOs or just send all for simplicity
|
||||
var list = new List<object>();
|
||||
for (int i = 0; i < 32; i++)
|
||||
{
|
||||
webView2.GoBack();
|
||||
list.Add(new { id = i, type = "input", state = inputs[i] });
|
||||
list.Add(new { id = i, type = "output", state = outputs[i] });
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private void BtnForward_Click(object sender, EventArgs e)
|
||||
private void FWebView_Load(object sender, EventArgs e)
|
||||
{
|
||||
if (webView2.CanGoForward)
|
||||
{
|
||||
webView2.GoForward();
|
||||
}
|
||||
// Setup Simulation Timer (50ms)
|
||||
plcTimer = new Timer();
|
||||
plcTimer.Interval = 50;
|
||||
plcTimer.Tick += PlcTimer_Tick;
|
||||
plcTimer.Start();
|
||||
}
|
||||
|
||||
private void BtnRefresh_Click(object sender, EventArgs e)
|
||||
// --- Helper Methods called by Bridge ---
|
||||
public void SetTargetPosition(string axis, double val)
|
||||
{
|
||||
webView2.Reload();
|
||||
if (axis == "X") targetX = val;
|
||||
if (axis == "Y") targetY = val;
|
||||
if (axis == "Z") targetZ = val;
|
||||
}
|
||||
|
||||
// JavaScript 실행 예제 메서드
|
||||
public void SetOutput(int id, bool state)
|
||||
{
|
||||
if (id < 32) outputs[id] = state;
|
||||
}
|
||||
|
||||
public void HandleCommand(string cmd)
|
||||
{
|
||||
systemState = (cmd == "START") ? "RUNNING" : (cmd == "STOP") ? "PAUSED" : "IDLE";
|
||||
}
|
||||
|
||||
public void SetCurrentRecipe(string recipeId)
|
||||
{
|
||||
currentRecipeId = recipeId;
|
||||
Console.WriteLine($"[fWebView] Current recipe set to: {recipeId}");
|
||||
// In real app, load recipe parameters here
|
||||
}
|
||||
|
||||
public string GetCurrentRecipe()
|
||||
{
|
||||
return currentRecipeId;
|
||||
}
|
||||
|
||||
private double Lerp(double a, double b, double t) => a + (b - a) * t;
|
||||
|
||||
// JavaScript 실행 메서드
|
||||
public async void ExecuteScriptAsync(string script)
|
||||
{
|
||||
try
|
||||
{
|
||||
string result = await webView2.CoreWebView2.ExecuteScriptAsync(script);
|
||||
MessageBox.Show($"실행 결과: {result}");
|
||||
string result = await webView.CoreWebView2.ExecuteScriptAsync(script);
|
||||
Console.WriteLine($"Script result: {result}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"스크립트 실행 실패: {ex.Message}", "Error",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
Console.WriteLine($"Script execution failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,23 +207,12 @@ namespace Project.Dialog
|
||||
{
|
||||
try
|
||||
{
|
||||
webView2.CoreWebView2.PostWebMessageAsString(message);
|
||||
webView.CoreWebView2.PostWebMessageAsString(message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"메시지 전송 실패: {ex.Message}", "Error",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
Console.WriteLine($"PostMessage failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// JavaScript에서 C#으로 메시지 수신
|
||||
private void SetupWebMessageReceived()
|
||||
{
|
||||
webView2.CoreWebView2.WebMessageReceived += (sender, e) =>
|
||||
{
|
||||
string message = e.TryGetWebMessageAsString();
|
||||
MessageBox.Show($"웹에서 받은 메시지: {message}");
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user