using AppTime.Properties; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data.SQLite; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace AppTime { class Recorder { public const string ExName = "mkv"; public Recorder() { BuildDataPath(); } /// /// 统计周期。 /// public int IntervalMs = 1000; public void Start() { new Thread(RecorderThreadProc) { IsBackground = true }.Start(); } class App { public string WinText; public string AppProcess; public DateTime TimeStart; public long WinId; } public void BuildDataPath() { Directory.CreateDirectory(IconPath); Directory.CreateDirectory(ScreenPath); } Dictionary processes; public Process GetProcess(int processID) { if (processes != null && processes.TryGetValue(processID, out var p)) { return p; } processes = Process.GetProcesses().ToDictionary(p => p.Id); return processes[processID]; } class app { public long id; public string process; public Dictionary wins = new Dictionary(); } class win { public long id; public string text; } long nextAppId = 0; Icon GetIcon(string fileName, bool largeIcon) { var shfi = new SHFILEINFO(); WinApi.SHGetFileInfo(fileName, 0, ref shfi, (uint)Marshal.SizeOf(shfi), (uint)FileInfoFlags.SHGFI_ICON | (uint)FileInfoFlags.SHGFI_USEFILEATTRIBUTES | (uint)(largeIcon ? FileInfoFlags.SHGFI_LARGEICON : FileInfoFlags.SHGFI_SMALLICON) ); return Icon.FromHandle(shfi.hIcon); } void SaveIcon(Icon icon, string filename) { using var img = icon.ToBitmap(); img.Save(filename); } public string GetIconPath(long appId, bool large) { return Path.Combine(IconPath, $"{appId}{(large ? "l" : "s")}.png"); } Dictionary apps = new Dictionary(); app GetApp(Process process) { var name = process.ProcessName; if (apps.TryGetValue(name, out var app)) { return app; } var data = db.ExecuteDynamic( @"select id from app where process = @process", new SQLiteParameter("process", name) ).FirstOrDefault(); if (data == null) { if (nextAppId == 0) { nextAppId = (int)(long)db.ExecuteData("select ifnull(max(id),0) + 1 from app")[0][0]; } var text = ""; try { text = process.MainModule.FileVersionInfo.FileDescription; using var iconl = GetIcon(process.MainModule.FileName, true); SaveIcon(iconl, GetIconPath(nextAppId, true)); using var icons = GetIcon(process.MainModule.FileName, false); SaveIcon(icons, GetIconPath(nextAppId, false)); } catch (Win32Exception) { //ignore } catch (FileNotFoundException) { //ignore } if (string.IsNullOrWhiteSpace(text)) { text = process.ProcessName; } db.Execute( "insert into app (id, process, text, tagId) values(@id, @process, @text, 0)", new SQLiteParameter("id", nextAppId), new SQLiteParameter("process", name), new SQLiteParameter("text", text) ); app = new app { id = nextAppId, process = name }; nextAppId++; } else { app = new app { id = data.id, process = name }; } apps.Add(name, app); //fix icons var largeIconPath = GetIconPath(app.id, true); if (!File.Exists(largeIconPath)) { try { using var iconl = GetIcon(process.MainModule.FileName, true); SaveIcon(iconl, largeIconPath); using var icons = GetIcon(process.MainModule.FileName, false); SaveIcon(icons, GetIconPath(app.id, false)); } catch(Win32Exception) { } } return app; } long nextWinId = 0; win GetWin(Process process, string winText) { var app = GetApp(process); if (app.wins.TryGetValue(winText, out var win)) { return win; } var data = db.ExecuteDynamic( "select id from win where appid=@appid and text=@winText", new SQLiteParameter("appid", app.id), new SQLiteParameter("winText", winText) ).FirstOrDefault(); if (data == null) { if (nextWinId == 0) { nextWinId = (int)(long)db.ExecuteData("select ifnull(max(id),0) + 1 from win")[0][0]; } db.Execute( "insert into win (id, appId, text) values(@id, @appId, @text)", new SQLiteParameter("id", nextWinId), new SQLiteParameter("appId", app.id), new SQLiteParameter("text", winText) ); win = new win { id = nextWinId, text = winText }; nextWinId++; } else { win = new win { id = data.id, text = winText }; } app.wins.Add(winText, win); return win; } DB db = DB.Instance; public void RecorderThreadProc() { App lastApp = null; while (true) { var now = DateTime.Now; var hwnd = WinApi.GetForegroundWindow(); var text = new StringBuilder(255); WinApi.GetWindowText(hwnd, text, 255); var winText = text.ToString(); WinApi.GetWindowThreadProcessId(hwnd, out var processid); var process = GetProcess(processid); var appname = process.ProcessName; if (lastApp != null) { db.Execute( "update period set timeend = @v1 where timestart = @v0", lastApp.TimeStart, now.AddMilliseconds(-1)//必须减小,否则可能与下个周期开始时间重叠 ); } if (lastApp == null || lastApp.AppProcess != appname || lastApp.WinText != winText) { var win = GetWin(process, winText); lastApp = new App { WinId = win.id, AppProcess = appname, TimeStart = now, WinText = winText }; db.Execute( "insert into [period](winid, timeStart, timeEnd) values(@v0, @v1, @v1)", win.id, now ); } Screenshot(now); //等到下一个周期 var nextTime = now.AddMilliseconds(IntervalMs); now = DateTime.Now; if(nextTime > now) { Thread.Sleep(nextTime - now); } } } string DataPath => string.IsNullOrWhiteSpace(Settings.Default.DataPath) ? Application.StartupPath : Settings.Default.DataPath; public string ScreenPath => Path.Combine(DataPath, "images"); public string IconPath => Path.Combine(DataPath, "icons"); ImageCodecInfo jpgcodec = ImageCodecInfo.GetImageDecoders().First(codec => codec.MimeType == "image/jpeg"); ///// ///// 获取图片文件路径 ///// ///// ///// ///// //public string getImageFile(DateTime timeStart, DateTime timeImage) //{ // var folder = Path.Combine(ScreenPath, timeImage.ToString("yyyyMMdd")); // var filename = $"{timeStart:HHmmss}+{Math.Round((timeImage - timeStart).TotalSeconds)}"; // return Path.Combine(folder, $"{filename}.jpg"); //} public string getFileName(DateTime time) { return Path.Combine(ScreenPath, $"{time:yyyyMMdd}", $"{time:HHmmss}." + Recorder.ExName); } DateTime lastCheck = DateTime.MinValue.Date; /// /// 截图 /// /// /// void Screenshot(DateTime now) { //检查记录天数限制 if (lastCheck != now.Date) { var firstDate = now.Date.AddDays(-Settings.Default.RecordScreenDays); var dirs = Directory.EnumerateDirectories(ScreenPath, "????????"); foreach (var i in dirs) { if (DateTime.TryParseExact(Path.GetFileName(i), "yyyyMMdd", CultureInfo.CurrentCulture, DateTimeStyles.None, out var date)) { if (date < firstDate) { Directory.Delete(i); } } } lastCheck = now.Date; } if (Settings.Default.RecordScreenDays == 0) { return; } if (buffer == null) { buffer = new MemoryBuffer(now); } using var img = GetScreen(); using var mem = new MemoryStream(); img.Save(mem, ImageFormat.Jpeg); buffer.Frames.Add(new Frame(now - buffer.StartTime, mem.ToArray())); if ((now - buffer.StartTime).TotalSeconds >= 10 * 60 || now.Date != buffer.StartTime.Date) //固定为10mins,防止保存时间长,减少出问题时影响的时长。 { FlushScreenBuffer(); } } public void FlushScreenBuffer() { //切换到新buffer var newBuffer = new MemoryBuffer(DateTime.Now); var b = buffer; buffer = newBuffer; //加入flushing lock (flushing) { flushing.Add(b); } var path = getFileName(b.StartTime); var folder = Path.GetDirectoryName(path); if (!Directory.Exists(folder)) { Directory.CreateDirectory(folder); } new Thread(() => { Ffmpeg.Save(getFileName(b.StartTime), b.Frames.ToArray()); lock (flushing) { flushing.Remove(b); } }) { Priority = ThreadPriority.BelowNormal, IsBackground = false }.Start(); } public class MemoryBuffer { public readonly DateTime StartTime; public readonly List Frames = new List(); public MemoryBuffer(DateTime startTime) { StartTime = startTime; } } public MemoryBuffer buffer; public List flushing = new List(); Bitmap GetScreen() { var result = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height); using var g = Graphics.FromImage(result); retry: try { g.CopyFromScreen(0, 0, 0, 0, Screen.PrimaryScreen.Bounds.Size); } catch { goto retry; } return result; } } }