mirror of
https://github.com/gnh1201/welsonjs.git
synced 2024-11-26 15:31:42 +00:00
Add Screen Matching feature
This commit is contained in:
parent
d5f55e0f28
commit
b687cf7727
|
@ -14,6 +14,11 @@ namespace WelsonJS.Service
|
|||
{
|
||||
if (Environment.UserInteractive)
|
||||
{
|
||||
Console.WriteLine("WelsonJS Service Application (User Interactive Mode)");
|
||||
Console.WriteLine("https://github.com/gnh1201/welsonjs");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Service is running...");
|
||||
|
||||
ServiceMain svc = new ServiceMain(args);
|
||||
svc.TestStartupAndStop();
|
||||
}
|
||||
|
|
22
WelsonJS.Toolkit/WelsonJS.Service/ScreenMatchResult.cs
Normal file
22
WelsonJS.Toolkit/WelsonJS.Service/ScreenMatchResult.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace WelsonJS.Service
|
||||
{
|
||||
public class ScreenMatchResult
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
public int ScreenNumber { get; set; }
|
||||
public IntPtr WindowHandle { get; set; }
|
||||
public string WindowTitle { get; set; }
|
||||
public Point Location { get; set; }
|
||||
public double MaxCorrelation { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Template: {FileName}, Screen Number: {ScreenNumber}, Window Title: {WindowTitle}, " +
|
||||
$"Location: (x: {Location.X}, y: {Location.Y}), " +
|
||||
$"Max Correlation: {MaxCorrelation}";
|
||||
}
|
||||
}
|
||||
}
|
227
WelsonJS.Toolkit/WelsonJS.Service/ScreenMatching.cs
Normal file
227
WelsonJS.Toolkit/WelsonJS.Service/ScreenMatching.cs
Normal file
|
@ -0,0 +1,227 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
using WelsonJS.Service;
|
||||
|
||||
public class ScreenMatching
|
||||
{
|
||||
// User32.dll API 함수 선언
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern int GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr GetDC(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern int GetWindowTextLength(IntPtr hWnd);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
private static extern bool BitBlt(IntPtr hDestDC, int x, int y, int nWidth, int nHeight, IntPtr hSrcDC, int xSrc, int ySrc, int dwRop);
|
||||
private const int SRCCOPY = 0x00CC0020;
|
||||
|
||||
// 델리게이트 선언
|
||||
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
|
||||
// RECT 구조체 선언
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct RECT
|
||||
{
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
|
||||
public List<Bitmap> templateImages;
|
||||
string templateFolderPath;
|
||||
|
||||
public ScreenMatching(string workingDirectory)
|
||||
{
|
||||
templateFolderPath = Path.Combine(workingDirectory, "app/assets/img/_templates");
|
||||
templateImages = new List<Bitmap>();
|
||||
LoadTemplateImages();
|
||||
}
|
||||
|
||||
public void LoadTemplateImages()
|
||||
{
|
||||
var files = System.IO.Directory.GetFiles(templateFolderPath, "*.png");
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
Bitmap bitmap = new Bitmap(file);
|
||||
bitmap.Tag = System.IO.Path.GetFileName(file);
|
||||
templateImages.Add(bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
// 화면을 기준으로 찾기
|
||||
public List<ScreenMatchResult> CaptureAndMatchAllScreens()
|
||||
{
|
||||
var results = new List<ScreenMatchResult>();
|
||||
|
||||
for (int i = 0; i < Screen.AllScreens.Length; i++)
|
||||
{
|
||||
Screen screen = Screen.AllScreens[i];
|
||||
Bitmap mainImage = CaptureScreen(screen);
|
||||
|
||||
foreach (Bitmap templateImage in templateImages)
|
||||
{
|
||||
Point matchLocation = FindTemplate(mainImage, (Bitmap)templateImage.Clone(), out double maxCorrelation);
|
||||
|
||||
results.Add(new ScreenMatchResult
|
||||
{
|
||||
FileName = templateImage.Tag.ToString(),
|
||||
ScreenNumber = i,
|
||||
Location = matchLocation,
|
||||
MaxCorrelation = maxCorrelation
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public static Bitmap CaptureScreen(Screen screen)
|
||||
{
|
||||
Rectangle screenSize = screen.Bounds;
|
||||
Bitmap bitmap = new Bitmap(screenSize.Width, screenSize.Height);
|
||||
|
||||
using (Graphics g = Graphics.FromImage(bitmap))
|
||||
{
|
||||
g.CopyFromScreen(screenSize.Left, screenSize.Top, 0, 0, screenSize.Size);
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
// 윈도우 핸들을 기준으로 찾기
|
||||
public List<ScreenMatchResult> CaptureAndMatchAllWindows()
|
||||
{
|
||||
var results = new List<ScreenMatchResult>();
|
||||
|
||||
// 모든 윈도우 핸들을 열거
|
||||
EnumWindows((hWnd, lParam) =>
|
||||
{
|
||||
if (IsWindowVisible(hWnd))
|
||||
{
|
||||
try
|
||||
{
|
||||
string windowTitle = GetWindowTitle(hWnd);
|
||||
Bitmap windowImage = CaptureWindow(hWnd);
|
||||
if (windowImage != null)
|
||||
{
|
||||
foreach (var templateImage in templateImages)
|
||||
{
|
||||
Point matchLocation = FindTemplate(windowImage, templateImage, out double maxCorrelation);
|
||||
string templateFileName = templateImage.Tag as string;
|
||||
|
||||
var result = new ScreenMatchResult
|
||||
{
|
||||
FileName = templateFileName,
|
||||
WindowHandle = hWnd,
|
||||
WindowTitle = windowTitle,
|
||||
Location = matchLocation,
|
||||
MaxCorrelation = maxCorrelation
|
||||
};
|
||||
results.Add(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return true;
|
||||
}, IntPtr.Zero);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public string GetWindowTitle(IntPtr hWnd)
|
||||
{
|
||||
int length = GetWindowTextLength(hWnd);
|
||||
StringBuilder sb = new StringBuilder(length + 1);
|
||||
GetWindowText(hWnd, sb, sb.Capacity);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public Bitmap CaptureWindow(IntPtr hWnd)
|
||||
{
|
||||
GetWindowRect(hWnd, out RECT rect);
|
||||
int width = rect.Right - rect.Left;
|
||||
int height = rect.Bottom - rect.Top;
|
||||
|
||||
if (width <= 0 || height <= 0)
|
||||
return null;
|
||||
|
||||
Bitmap bitmap = new Bitmap(width, height);
|
||||
Graphics graphics = Graphics.FromImage(bitmap);
|
||||
IntPtr hDC = graphics.GetHdc();
|
||||
IntPtr windowDC = GetDC(hWnd);
|
||||
|
||||
bool success = BitBlt(hDC, 0, 0, width, height, windowDC, 0, 0, SRCCOPY);
|
||||
ReleaseDC(hWnd, windowDC);
|
||||
graphics.ReleaseHdc(hDC);
|
||||
|
||||
return success ? bitmap : null;
|
||||
}
|
||||
|
||||
public Point FindTemplate(Bitmap mainImage, Bitmap templateImage, out double maxCorrelation)
|
||||
{
|
||||
int mainWidth = mainImage.Width;
|
||||
int mainHeight = mainImage.Height;
|
||||
int templateWidth = templateImage.Width;
|
||||
int templateHeight = templateImage.Height;
|
||||
|
||||
Point bestMatch = Point.Empty;
|
||||
maxCorrelation = 0;
|
||||
|
||||
for (int x = 0; x <= mainWidth - templateWidth; x++)
|
||||
{
|
||||
for (int y = 0; y <= mainHeight - templateHeight; y++)
|
||||
{
|
||||
if (IsTemplateMatch(mainImage, templateImage, x, y))
|
||||
{
|
||||
bestMatch = new Point(x, y);
|
||||
maxCorrelation = 1; // 완전 일치
|
||||
return bestMatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
private bool IsTemplateMatch(Bitmap mainImage, Bitmap templateImage, int offsetX, int offsetY)
|
||||
{
|
||||
int templateWidth = templateImage.Width;
|
||||
int templateHeight = templateImage.Height;
|
||||
|
||||
for (int x = 0; x < templateWidth; x++)
|
||||
{
|
||||
for (int y = 0; y < templateHeight; y++)
|
||||
{
|
||||
if (mainImage.GetPixel(x + offsetX, y + offsetY) != templateImage.GetPixel(x, y))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -34,39 +34,24 @@ namespace WelsonJS.Service
|
|||
{
|
||||
public partial class ServiceMain : ServiceBase
|
||||
{
|
||||
private Timer timer;
|
||||
private static List<Timer> timers;
|
||||
private string workingDirectory;
|
||||
private string scriptName;
|
||||
private string scriptFilePath;
|
||||
private string scriptText;
|
||||
private string scriptName;
|
||||
private ScriptControl scriptControl;
|
||||
private string logFilePath;
|
||||
private string[] _args;
|
||||
private readonly string logFilePath = Path.Combine(Path.GetTempPath(), "WelsonJS.Service.Log.txt");
|
||||
private readonly string appName = "WelsonJS";
|
||||
private string[] _args;
|
||||
private bool disabledScreenTime = false;
|
||||
private ScreenMatching screenMatcher;
|
||||
|
||||
public ServiceMain(string[] args)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// set the log file path
|
||||
logFilePath = Path.Combine(Path.GetTempPath(), "WelsonJS.Service.Log.txt");
|
||||
Log(appName + " Service Loaded");
|
||||
|
||||
// set service arguments
|
||||
// An auto-start service application should receive arguments at the class.
|
||||
_args = args;
|
||||
}
|
||||
|
||||
internal void TestStartupAndStop()
|
||||
{
|
||||
this.OnStart(_args);
|
||||
Console.ReadLine();
|
||||
this.OnStop();
|
||||
}
|
||||
|
||||
protected override void OnStart(string[] args)
|
||||
{
|
||||
base.OnStart(args);
|
||||
|
||||
// mapping arguments to each variables
|
||||
var arguments = ParseArguments(_args);
|
||||
|
@ -81,15 +66,22 @@ namespace WelsonJS.Service
|
|||
case "script-name":
|
||||
scriptName = entry.Value;
|
||||
break;
|
||||
|
||||
case "disable-screen-time":
|
||||
disabledScreenTime = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// set timers
|
||||
timers = new List<Timer>();
|
||||
|
||||
// set working directory
|
||||
if (string.IsNullOrEmpty(workingDirectory))
|
||||
{
|
||||
workingDirectory = Path.Combine(Path.GetTempPath(), appName);
|
||||
Log("Working directory not provided. Using default value: " + workingDirectory);
|
||||
|
||||
|
||||
if (!Directory.Exists(workingDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(workingDirectory);
|
||||
|
@ -108,6 +100,51 @@ namespace WelsonJS.Service
|
|||
// set path of the script
|
||||
scriptFilePath = Path.Combine(workingDirectory, "app.js");
|
||||
|
||||
// set default timer
|
||||
Timer defaultTimer = new Timer
|
||||
{
|
||||
Interval = 60000 // 1 minute
|
||||
};
|
||||
defaultTimer.Elapsed += OnElapsedTime;
|
||||
timers.Add(defaultTimer);
|
||||
|
||||
// set screen timer
|
||||
if (!disabledScreenTime && Environment.UserInteractive) {
|
||||
screenMatcher = new ScreenMatching(workingDirectory);
|
||||
|
||||
Timer screenTimer = new Timer
|
||||
{
|
||||
Interval = 1000 // 1 second
|
||||
};
|
||||
screenTimer.Elapsed += OnScreenTime;
|
||||
timers.Add(screenTimer);
|
||||
|
||||
Log("Screen Time Event Enabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
disabledScreenTime = true;
|
||||
|
||||
Log("Screen Time Event Disabled");
|
||||
}
|
||||
|
||||
// set the log file path
|
||||
logFilePath = Path.Combine(Path.GetTempPath(), "WelsonJS.Service.Log.txt");
|
||||
Log(appName + " Service Loaded");
|
||||
}
|
||||
|
||||
internal void TestStartupAndStop()
|
||||
{
|
||||
this.OnStart(_args);
|
||||
Console.ReadLine();
|
||||
this.OnStop();
|
||||
}
|
||||
|
||||
protected override void OnStart(string[] args)
|
||||
{
|
||||
base.OnStart(args);
|
||||
|
||||
|
||||
// check the script file exists
|
||||
if (File.Exists(scriptFilePath))
|
||||
{
|
||||
|
@ -126,7 +163,7 @@ namespace WelsonJS.Service
|
|||
scriptControl.AddCode(scriptText);
|
||||
|
||||
// initialize
|
||||
Log(DispatchServiceEvent(scriptName, "start"));
|
||||
Log(DispatchServiceEvent("start"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -138,22 +175,18 @@ namespace WelsonJS.Service
|
|||
Log($"Script file not found: {scriptFilePath}");
|
||||
}
|
||||
|
||||
// set interval
|
||||
timer = new Timer();
|
||||
timer.Interval = 60000; // 1 minute
|
||||
timer.Elapsed += OnElapsedTime;
|
||||
timer.Start();
|
||||
timers.ForEach(timer => timer.Start()); // start
|
||||
|
||||
Log(appName + " Service Started");
|
||||
}
|
||||
|
||||
protected override void OnStop()
|
||||
{
|
||||
timer.Stop();
|
||||
timers.ForEach(timer => timer.Stop()); // stop
|
||||
|
||||
try
|
||||
{
|
||||
Log(DispatchServiceEvent(scriptName, "stop"));
|
||||
Log(DispatchServiceEvent("stop"));
|
||||
if (scriptControl != null)
|
||||
{
|
||||
scriptControl.Reset();
|
||||
|
@ -172,7 +205,7 @@ namespace WelsonJS.Service
|
|||
{
|
||||
try
|
||||
{
|
||||
Log(DispatchServiceEvent(scriptName, "elapsedTime"));
|
||||
Log(DispatchServiceEvent("elapsedTime"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -180,9 +213,36 @@ namespace WelsonJS.Service
|
|||
}
|
||||
}
|
||||
|
||||
private string DispatchServiceEvent(string name, string eventType)
|
||||
private void OnScreenTime(object source, ElapsedEventArgs e)
|
||||
{
|
||||
return InvokeScriptMethod("dispatchServiceEvent", name, eventType);
|
||||
try
|
||||
{
|
||||
List<ScreenMatchResult> matchedResults = screenMatcher.CaptureAndMatchAllScreens();
|
||||
matchedResults.ForEach(result =>
|
||||
{
|
||||
Log(result.FileName);
|
||||
Log(result.ScreenNumber.ToString());
|
||||
Log(result.Location.ToString());
|
||||
|
||||
Log(DispatchServiceEvent("screenTime", new object[]
|
||||
{
|
||||
result.FileName,
|
||||
result.ScreenNumber.ToString(),
|
||||
result.Location.X.ToString(),
|
||||
result.Location.Y.ToString(),
|
||||
result.MaxCorrelation.ToString()
|
||||
}));
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log("Exception when screen time: " + ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private string DispatchServiceEvent(string eventType, object[] args = null)
|
||||
{
|
||||
return InvokeScriptMethod("dispatchServiceEvent", scriptName, eventType, args);
|
||||
}
|
||||
|
||||
private string InvokeScriptMethod(string methodName, params object[] parameters)
|
||||
|
@ -202,9 +262,16 @@ namespace WelsonJS.Service
|
|||
|
||||
private void Log(string message)
|
||||
{
|
||||
string _message = $"{DateTime.Now}: {message}";
|
||||
|
||||
if (Environment.UserInteractive)
|
||||
{
|
||||
Console.WriteLine(_message);
|
||||
}
|
||||
|
||||
using (StreamWriter writer = new StreamWriter(logFilePath, true))
|
||||
{
|
||||
writer.WriteLine($"{DateTime.Now}: {message}");
|
||||
writer.WriteLine(_message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,6 +290,11 @@ namespace WelsonJS.Service
|
|||
var value = arg.Substring(index + 1);
|
||||
arguments[key] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
var key = arg.Substring(2, index - 2);
|
||||
arguments[key] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{09F295EE-5EDB-4327-ABBE-DDCCDF5EDD9E}</ProjectGuid>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>WelsonJS.Service</RootNamespace>
|
||||
<AssemblyName>WelsonJS.Service</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
|
@ -83,8 +83,10 @@
|
|||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration.Install" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Management" />
|
||||
<Reference Include="System.ServiceProcess" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -102,6 +104,8 @@
|
|||
<DependentUpon>ProjectInstaller.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="ScreenMatching.cs" />
|
||||
<Compile Include="ScreenMatchResult.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<COMReference Include="MSScriptControl">
|
||||
|
|
10
app.js
10
app.js
|
@ -589,7 +589,7 @@ function initializeWindow(name, args, w, h) {
|
|||
}
|
||||
}
|
||||
|
||||
function dispatchServiceEvent(name, eventType) {
|
||||
function dispatchServiceEvent(name, eventType, args) {
|
||||
var app = require(name);
|
||||
|
||||
// load the service
|
||||
|
@ -597,8 +597,9 @@ function dispatchServiceEvent(name, eventType) {
|
|||
return (function(action) {
|
||||
if (eventType in action) {
|
||||
try {
|
||||
var f = action[eventType];
|
||||
if (typeof f === "function") return f();
|
||||
return (function(f) {
|
||||
return (typeof f !== "function" ? null : f(args));
|
||||
})(action[eventType]);
|
||||
} catch (e) {
|
||||
console.error("Exception:", e.message);
|
||||
}
|
||||
|
@ -606,7 +607,8 @@ function dispatchServiceEvent(name, eventType) {
|
|||
})({
|
||||
start: app.onServiceStart,
|
||||
stop: app.onServiceStop,
|
||||
elapsedTime: app.onServiceElapsedTime
|
||||
elapsedTime: app.onServiceElapsedTime,
|
||||
screenTime: app.onServiceScreenTime
|
||||
});
|
||||
} else {
|
||||
console.error("Could not find", name + ".js");
|
||||
|
|
BIN
app/assets/img/_templates/2024-07-29 19 56 13.png
Normal file
BIN
app/assets/img/_templates/2024-07-29 19 56 13.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
Binary file not shown.
Binary file not shown.
|
@ -14,7 +14,12 @@ function onServiceElapsedTime() {
|
|||
return "onServiceElapsedTime recevied";
|
||||
}
|
||||
|
||||
function onServiceScreenTime(filename, handle, title, x, y, maxCorrelation) {
|
||||
return "onServiceScreenTime recevied. " + filename;
|
||||
}
|
||||
|
||||
exports.main = main;
|
||||
exports.onServiceStart = onServiceStart;
|
||||
exports.onServiceStop = onServiceStop;
|
||||
exports.onServiceElapsedTime = onServiceElapsedTime;
|
||||
exports.onServiceScreenTime = onServiceScreenTime;
|
||||
|
|
Loading…
Reference in New Issue
Block a user