Add Screen Matching feature

This commit is contained in:
Namhyeon Go 2024-07-29 21:43:14 +09:00
parent d5f55e0f28
commit b687cf7727
10 changed files with 376 additions and 39 deletions

View File

@ -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();
}

View 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}";
}
}
}

View 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;
}
}

View File

@ -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] = "";
}
}
}

View File

@ -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
View File

@ -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");

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Binary file not shown.

View File

@ -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;