294 lines
10 KiB
C#
294 lines
10 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Windows.Interop;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Imaging;
|
|
using System.Windows.Threading;
|
|
using Paca.App.Core;
|
|
|
|
namespace Paca.App.Services;
|
|
|
|
/// <summary>
|
|
/// PACA 데스크톱 애플리케이션 화면 디버깅 캡처 서비스.
|
|
/// Win32 GDI / PrintWindow / RenderTargetBitmap 하이브리드 파이프라인으로
|
|
/// WPF 및 WebView2(HWND) 자식 컨트롤이 포함된 실제 앱 윈도우 전체를 스크린샷으로 캡처하여 저장한다.
|
|
/// </summary>
|
|
public static class AppDebugScreenshotService
|
|
{
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct RECT
|
|
{
|
|
public int Left;
|
|
public int Top;
|
|
public int Right;
|
|
public int Bottom;
|
|
}
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBkgnd, uint nFlags);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool BringWindowToTop(IntPtr hWnd);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
|
|
private const int SW_RESTORE = 9;
|
|
private const uint PW_RENDERFULLCONTENT = 0x00000002;
|
|
private static DispatcherTimer? _triggerTimer;
|
|
|
|
public static string GetDefaultScreenshotPath() =>
|
|
Path.Combine(AppPaths.DataDir, "debug_screenshot.png");
|
|
|
|
public static string ResolveTriggerPath(string? input)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(input))
|
|
return GetDefaultScreenshotPath();
|
|
return input.Trim();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 대상 Window의 현재 화면을 PNG 파일로 캡처하여 저장한다.
|
|
/// </summary>
|
|
public static bool CaptureWindow(Window window, string outputPath)
|
|
{
|
|
if (window == null) return false;
|
|
|
|
try
|
|
{
|
|
var dir = Path.GetDirectoryName(outputPath);
|
|
if (!string.IsNullOrEmpty(dir))
|
|
{
|
|
Directory.CreateDirectory(dir);
|
|
}
|
|
|
|
var helper = new WindowInteropHelper(window);
|
|
var hWnd = helper.Handle;
|
|
|
|
if (hWnd != IntPtr.Zero && GetWindowRect(hWnd, out var rect))
|
|
{
|
|
try
|
|
{
|
|
ShowWindow(hWnd, SW_RESTORE);
|
|
SetForegroundWindow(hWnd);
|
|
BringWindowToTop(hWnd);
|
|
System.Threading.Thread.Sleep(200);
|
|
GetWindowRect(hWnd, out rect);
|
|
}
|
|
catch { }
|
|
|
|
int width = rect.Right - rect.Left;
|
|
int height = rect.Bottom - rect.Top;
|
|
|
|
if (width > 0 && height > 0)
|
|
{
|
|
try
|
|
{
|
|
using var bmp = new System.Drawing.Bitmap(width, height);
|
|
using (var g = System.Drawing.Graphics.FromImage(bmp))
|
|
{
|
|
var hdc = g.GetHdc();
|
|
try
|
|
{
|
|
PrintWindow(hWnd, hdc, PW_RENDERFULLCONTENT);
|
|
}
|
|
finally
|
|
{
|
|
g.ReleaseHdc(hdc);
|
|
}
|
|
|
|
// WebView2 자식 컨트롤 실시간 렌더링 화면 비트맵 오버레이
|
|
OverlayAdBanners(window, g, width, height);
|
|
}
|
|
|
|
bmp.Save(outputPath, System.Drawing.Imaging.ImageFormat.Png);
|
|
if (File.Exists(outputPath) && new FileInfo(outputPath).Length > 0)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// GDI 캡처 실패 시 하단 WPF RenderTargetBitmap 폴백 진행
|
|
}
|
|
}
|
|
}
|
|
|
|
// 폴백: 순수 WPF 비주얼 트리 렌더링
|
|
int rtbWidth = (int)Math.Max(1, window.ActualWidth > 0 ? window.ActualWidth : window.Width);
|
|
int rtbHeight = (int)Math.Max(1, window.ActualHeight > 0 ? window.ActualHeight : window.Height);
|
|
if (rtbWidth <= 1) rtbWidth = 1024;
|
|
if (rtbHeight <= 1) rtbHeight = 768;
|
|
|
|
var rtb = new RenderTargetBitmap(rtbWidth, rtbHeight, 96, 96, PixelFormats.Pbgra32);
|
|
rtb.Render(window);
|
|
|
|
var encoder = new PngBitmapEncoder();
|
|
encoder.Frames.Add(BitmapFrame.Create(rtb));
|
|
using (var fs = File.Create(outputPath))
|
|
{
|
|
encoder.Save(fs);
|
|
}
|
|
|
|
return File.Exists(outputPath) && new FileInfo(outputPath).Length > 0;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 외부 신호 파일(%AppData%/VideoDownloader/screenshot_trigger.req)을 감지하여
|
|
/// 백그라운드에서 실시간 스크린샷을 찍도록 타이머를 구동한다.
|
|
/// </summary>
|
|
public static void StartFileTriggerWatcher(Window window, Action<string>? onCaptured = null)
|
|
{
|
|
if (_triggerTimer != null) return;
|
|
|
|
var triggerFile = Path.Combine(AppPaths.DataDir, "screenshot_trigger.req");
|
|
|
|
_triggerTimer = new DispatcherTimer
|
|
{
|
|
Interval = TimeSpan.FromSeconds(1)
|
|
};
|
|
|
|
_triggerTimer.Tick += (_, _) =>
|
|
{
|
|
try
|
|
{
|
|
if (File.Exists(triggerFile))
|
|
{
|
|
string target = "";
|
|
try
|
|
{
|
|
target = File.ReadAllText(triggerFile);
|
|
File.Delete(triggerFile);
|
|
}
|
|
catch
|
|
{
|
|
// 읽기 경합 방어
|
|
return;
|
|
}
|
|
|
|
var savePath = ResolveTriggerPath(target);
|
|
bool ok = CaptureWindow(window, savePath);
|
|
if (ok)
|
|
{
|
|
onCaptured?.Invoke(savePath);
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// 백그라운드 폴러 방어
|
|
}
|
|
};
|
|
|
|
_triggerTimer.Start();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 앱 실행 후 指定된 지연 시간(기본 3초) 뒤에 자동으로 스크린샷을 찍어 저장한다.
|
|
/// </summary>
|
|
public static void ScheduleAutoCapture(Window window, int delayMs = 3000, string? targetPath = null)
|
|
{
|
|
Task.Delay(delayMs).ContinueWith(_ =>
|
|
{
|
|
window.Dispatcher.Invoke(() =>
|
|
{
|
|
var dest = ResolveTriggerPath(targetPath);
|
|
CaptureWindow(window, dest);
|
|
});
|
|
});
|
|
}
|
|
|
|
private static void OverlayAdBanners(Window window, System.Drawing.Graphics g, int windowWidth, int windowHeight)
|
|
{
|
|
try
|
|
{
|
|
window.Dispatcher.Invoke(() =>
|
|
{
|
|
var banners = FindVisualChildren<Paca.App.Controls.DesktopAdBannerControl>(window);
|
|
foreach (var banner in banners)
|
|
{
|
|
if (banner.Visibility != Visibility.Visible || banner.ActualWidth <= 0 || banner.ActualHeight <= 0)
|
|
continue;
|
|
|
|
var previewPath = Path.Combine(AppPaths.DataDir, "ad_previews", $"ad_{banner.Placement}.png");
|
|
if (!File.Exists(previewPath)) continue;
|
|
|
|
try
|
|
{
|
|
var transform = banner.TransformToAncestor(window);
|
|
var point = transform.Transform(new Point(0, 0));
|
|
|
|
using var fs = new FileStream(previewPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
using var adImg = System.Drawing.Image.FromStream(fs);
|
|
int x = (int)point.X;
|
|
int y = (int)point.Y;
|
|
int w = (int)banner.ActualWidth;
|
|
int h = (int)banner.ActualHeight;
|
|
|
|
if (banner.Placement == Paca.Browser.Ads.DesktopAdPlacement.BottomAppBanner)
|
|
{
|
|
x += (w - 728) / 2;
|
|
y += (h - 90) / 2;
|
|
w = 728;
|
|
h = 90;
|
|
}
|
|
else if (banner.Placement == Paca.Browser.Ads.DesktopAdPlacement.DownloadViewBanner)
|
|
{
|
|
x += (w - 300) / 2;
|
|
y += (h - 250) / 2;
|
|
w = 300;
|
|
h = 250;
|
|
}
|
|
|
|
g.DrawImage(adImg, x, y, w, h);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
try
|
|
{
|
|
var p = Path.Combine(AppPaths.DataDir, "ad_debug.log");
|
|
File.AppendAllText(p, $"[{DateTime.Now:HH:mm:ss.fff}] Overlay exception for {banner.Placement}: {ex.Message}\n");
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
|
|
{
|
|
if (depObj == null) yield break;
|
|
int count = VisualTreeHelper.GetChildrenCount(depObj);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var child = VisualTreeHelper.GetChild(depObj, i);
|
|
if (child is T match)
|
|
{
|
|
yield return match;
|
|
}
|
|
|
|
foreach (var childOfChild in FindVisualChildren<T>(child))
|
|
{
|
|
yield return childOfChild;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|