1
0
Fork 0
mirror of https://github.com/hawkeye-stan/msfs-popout-panel-manager.git synced 2024-10-16 06:00:45 +00:00

Version 2.2

This commit is contained in:
hawkeye 2021-11-24 10:57:29 -05:00 committed by hawkeye
parent 4ca7a8e7b2
commit 9aa8f1d2db
19 changed files with 451 additions and 159 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View file

@ -4,8 +4,8 @@
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<Platforms>x64</Platforms>
<Version>2.1.1.0</Version>
<Platforms>x64;AnyCPU</Platforms>
<Version>2.2.0.0</Version>
<AssemblyName>MSFSPopoutPanelManager</AssemblyName>
<RootNamespace>MSFSPopoutPanelManager</RootNamespace>
<ApplicationIcon>WindowManager.ico</ApplicationIcon>
@ -13,8 +13,10 @@
<Product>MSFS 2020 Popout Panel Manager</Product>
<PackageId>MSFS 2020 Popout Panel Manager</PackageId>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<AssemblyVersion>2.1.1.0</AssemblyVersion>
<FileVersion>2.1.1.0</FileVersion>
<AssemblyVersion>2.2.0.0</AssemblyVersion>
<FileVersion>2.2.0.0</FileVersion>
<DebugType>None</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<ItemGroup>
@ -40,6 +42,9 @@
<None Remove="C:\Users\hawkeye\.nuget\packages\tesseract\4.1.1\build\\..\x86\tesseract41.dll" />
<None Remove="Config\AnalysisData\analysisconfig.json" />
<None Remove="Config\planeprofile.json" />
<None Remove="LICENSE" />
<None Remove="README.md" />
<None Remove="VERSION.md" />
</ItemGroup>
<ItemGroup>
@ -48,6 +53,7 @@
<PackageReference Include="DarkUI" Version="2.0.2" />
<PackageReference Include="MouseKeyHook" Version="5.6.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
@ -65,6 +71,15 @@
<Content Include="Config\AnalysisData\analysisconfig.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="LICENSE">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="README.md">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="VERSION.md">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
@ -206,10 +221,13 @@
<None Update="Config\AnalysisData\g1000nxi\g1000nxi_pfd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\preprocessingdata\separation_button.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\PreprocessingData\separation_button_qhd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\PreprocessingData\separation_button_uhd.png">
<None Update="Config\preprocessingdata\test.svg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\preprocess\blockmatch_wqhd1.png">
@ -236,27 +254,15 @@
<None Update="config\preprocess\g1000_pfd_wqhd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\PreprocessingData\separation_button_hd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\preprocessingdata\separation_button_wqhd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="images\transparent.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="LICENSE">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="VERSION.md">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Vesion.md">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="README.md">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
@ -271,10 +277,6 @@
<NativeLibs Remove="Logger.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Config\PreprocessingData\separation_button_hd.png" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Config\PreprocessingData\separation_button_wqhd.png" />
</ItemGroup>

View file

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31424.327
# Visual Studio Version 17
VisualStudioVersion = 17.0.31919.166
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSFSPopoutPanelManager", "MSFSPopoutPanelManager.csproj", "{1E89B7B3-DBD9-4644-A0EB-26924317DD83}"
EndProject

191
Modules/Autostart.cs Normal file
View file

@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
namespace MSFSPopoutPanelManager
{
public class Autostart
{
public static void Activate()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var filePath = GetFilePath();
var autoStartArg = new LaunchAddOn() { Name = "MSFS Popout Panel Manager", Disabled = "false", Path = $@"{Directory.GetCurrentDirectory()}\MSFSPopoutPanelManager.exe" };
XmlSerializer serializer = new XmlSerializer(typeof(SimBaseDocument));
if (filePath != null && File.Exists(filePath))
{
// Create backup file if needed
if (!File.Exists(filePath + ".backup"))
{
File.Copy(filePath, filePath + ".backup");
}
SimBaseDocument data;
using (Stream stream = new FileStream(filePath, FileMode.Open))
{
data = (SimBaseDocument)serializer.Deserialize(stream);
}
if (data != null)
{
if (data.LaunchAddOn.Count == 0)
{
data.LaunchAddOn.Add(autoStartArg);
}
else
{
var autoStartIndex = data.LaunchAddOn.FindIndex(x => x.Name == "MSFS Popout Panel Manager");
if (autoStartIndex > -1)
data.LaunchAddOn[autoStartIndex] = autoStartArg;
else
data.LaunchAddOn.Add(autoStartArg);
}
using (Stream stream = new FileStream(filePath, FileMode.Open))
{
stream.SetLength(0);
serializer.Serialize(stream, data, new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty }));
}
}
}
else
{
var data = new SimBaseDocument
{
Descr = "SimConnect",
Filename = "SimConnect.xml",
Disabled = "False",
Type = "SimConnecct",
Version = "1,0",
LaunchAddOn = new List<LaunchAddOn>() { autoStartArg }
};
using (var file = File.Create(filePath))
{
serializer.Serialize(file, data, new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty }));
}
}
}
public static void Deactivate()
{
//Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var filePath = GetFilePath();
if (filePath != null && File.Exists(filePath))
{
SimBaseDocument data;
XmlSerializer serializer = new XmlSerializer(typeof(SimBaseDocument));
using (Stream stream = new FileStream(filePath, FileMode.Open))
{
data = (SimBaseDocument)serializer.Deserialize(stream);
}
if (data != null)
{
if (data.LaunchAddOn.Count > 0)
{
var autoStartIndex = data.LaunchAddOn.FindIndex(x => x.Name == "MSFS Popout Panel Manager");
if (autoStartIndex > -1)
data.LaunchAddOn.RemoveAt(autoStartIndex);
}
using (Stream stream = new FileStream(filePath, FileMode.Open))
{
stream.SetLength(0);
serializer.Serialize(stream, data, new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty }));
}
}
}
}
public static bool CheckIsAutoStart()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var filePath = GetFilePath();
if (filePath != null && File.Exists(filePath))
{
SimBaseDocument data;
XmlSerializer serializer = new XmlSerializer(typeof(SimBaseDocument));
using (Stream stream = new FileStream(filePath, FileMode.Open))
{
data = (SimBaseDocument)serializer.Deserialize(stream);
}
if (data != null)
{
if (data.LaunchAddOn.Count > 0)
{
var autoStartIndex = data.LaunchAddOn.FindIndex(x => x.Name == "MSFS Popout Panel Manager");
if (autoStartIndex > -1)
return true;
else
return false;
}
}
}
return false;
}
private static string GetFilePath()
{
var filePathMSStore = Environment.ExpandEnvironmentVariables("%LocalAppData%") + @"\Packages\Microsoft.FlightSimulator_8wekyb3d8bbwe\LocalCache\";
var filePathSteam = Environment.ExpandEnvironmentVariables("%AppData%") + @"\Microsoft Flight Simulator\LocalCache\";
if (Directory.Exists(filePathMSStore))
return filePathMSStore + "exe.xml";
else if (Directory.Exists(filePathSteam))
return filePathSteam + "exe.xml";
else
return null;
}
}
[XmlRoot(ElementName = "SimBase.Document")]
public class SimBaseDocument
{
[XmlAttribute("Type")]
public string Type { get; set; }
[XmlAttribute("version")]
public string Version { get; set; }
[XmlElement(ElementName = "Descr")]
public string Descr { get; set; }
[XmlElement(ElementName = "Filename")]
public string Filename { get; set; }
[XmlElement(ElementName = "Disabled")]
public string Disabled { get; set; }
[XmlElement(ElementName = "Launch.Addon")]
public List<LaunchAddOn> LaunchAddOn { get; set; }
}
public class LaunchAddOn
{
[XmlElement(ElementName = "Name")]
public string Name { get; set; }
[XmlElement(ElementName = "Disabled")]
public string Disabled { get; set; }
[XmlElement(ElementName = "Path")]
public string Path { get; set; }
}
}

View file

@ -273,7 +273,7 @@ namespace MSFSPopoutPanelManager
return files;
}
private static string GetFilePathByType(FilePathType filePathType)
public static string GetFilePathByType(FilePathType filePathType)
{
switch (filePathType)
{

View file

@ -1,25 +1,43 @@
using AForge.Imaging;
using System;
using System.Drawing;
using System.Linq;
namespace MSFSPopoutPanelManager
{
public class ImageAnalysis
{
public static Point ExhaustiveTemplateMatchAnalysis(Bitmap sourceImage, Bitmap templateImage, int imageShrinkFactor, float similarityThreshHold)
public static Point ExhaustiveTemplateMatchAnalysisAsync(Bitmap sourceImage, Bitmap templateImage, float similarityThreshHold, int panelStartingTop, int panelStartingLeft)
{
var x = panelStartingLeft - Convert.ToInt32(templateImage.Width * 1.5);
var y = 0;
var width = Convert.ToInt32(templateImage.Width * 1.5);
var height = sourceImage.Height;
var searchZone = new Rectangle(x, y, width, height);
var point = AnalyzeExpandImageBitmap(sourceImage, templateImage, similarityThreshHold, searchZone);
if (point != Point.Empty)
point.Y += panelStartingTop;
return point;
}
public static Point AnalyzeExpandImageBitmap(Bitmap sourceImage, Bitmap templateImage, float similarityThreshHold, Rectangle searchZone)
{
// Full image pixel to pixel matching algorithm
ExhaustiveTemplateMatching etm = new ExhaustiveTemplateMatching(similarityThreshHold);
TemplateMatch[] templateMatches = etm.ProcessImage(sourceImage, templateImage);
TemplateMatch[] templateMatches = etm.ProcessImage(sourceImage, templateImage, searchZone);
// Highlight the matchings that were found and saved a copy of the highlighted image
if (templateMatches != null && templateMatches.Length > 0)
{
var match = templateMatches.OrderByDescending(x => x.Similarity).First(); // Just look at the first match since only one operation can be accomplished at a time on MSFS side
var match = templateMatches.OrderByDescending(x => x.Similarity).First(); // Just look at the first match
var x = match.Rectangle.X * imageShrinkFactor + templateImage.Width * imageShrinkFactor / 4;
var y = match.Rectangle.Y * imageShrinkFactor + templateImage.Height * imageShrinkFactor / 4;
var xCoor = match.Rectangle.X + templateImage.Width / 12;
var yCoor = match.Rectangle.Y + templateImage.Height / 4;
return new Point(x, y);
return new Point(Convert.ToInt32(xCoor), Convert.ToInt32(yCoor));
}
return Point.Empty;
@ -38,19 +56,6 @@ namespace MSFSPopoutPanelManager
var templateMatches = bm.ProcessImage(sourceImage, points, templateImage);
return templateMatches.Count;
// Full image pixel to pixel matching algorithm
//ExhaustiveTemplateMatching etm = new ExhaustiveTemplateMatching(similarityThreshHold);
//TemplateMatch[] templateMatches = etm.ProcessImage(sourceImage, templateImage);
//// Highlight the matchings that were found and saved a copy of the highlighted image
//if (templateMatches != null && templateMatches.Length > 0)
//{
// var imageMatched = templateMatches.ToList().Max(x => x.Similarity);
// return imageMatched;
//}
//return 0;
}
}
}

View file

@ -22,9 +22,10 @@ namespace MSFSPopoutPanelManager
return new Bitmap(new MemoryStream(bytes));
}
public static Bitmap ResizeImage(Bitmap sourceImage, float width, float height)
public static Bitmap ResizeImage(Bitmap sourceImage, double width, double height)
{
return new ResizeBilinear(Convert.ToInt32(width), Convert.ToInt32(height)).Apply(sourceImage);
var bmp = new ResizeBilinear(Convert.ToInt32(width), Convert.ToInt32(height)).Apply(sourceImage);
return ImageOperation.ConvertToFormat(bmp, PixelFormat.Format24bppRgb);
}
public static Bitmap CropImage(Bitmap sourceImage, int x, int y, int width, int height)
@ -36,7 +37,8 @@ namespace MSFSPopoutPanelManager
{
gr.DrawImage(sourceImage, new Rectangle(0, 0, bmp.Width, bmp.Height), crop, GraphicsUnit.Pixel);
}
return bmp;
return ImageOperation.ConvertToFormat(bmp, PixelFormat.Format24bppRgb);
}
public static Bitmap ConvertToFormat(Bitmap image, PixelFormat format)
@ -46,6 +48,7 @@ namespace MSFSPopoutPanelManager
{
gr.DrawImage(image, new Rectangle(0, 0, copy.Width, copy.Height));
}
return copy;
}
@ -70,14 +73,14 @@ namespace MSFSPopoutPanelManager
var bottom = rect.Bottom;
var bounds = new Rectangle(left, top, right - left, bottom - top);
var bitmap = new Bitmap(bounds.Width, bounds.Height);
var bmp = new Bitmap(bounds.Width, bounds.Height);
using (Graphics g = Graphics.FromImage(bitmap))
using (Graphics g = Graphics.FromImage(bmp))
{
g.CopyFromScreen(new Point(bounds.Left, bounds.Top), Point.Empty, bounds.Size);
}
return bitmap;
return ImageOperation.ConvertToFormat(bmp, PixelFormat.Format24bppRgb);
}
public static Bitmap HighLightMatchedPattern(Bitmap sourceImage, List<Rect> rectBoxes)
@ -96,5 +99,12 @@ namespace MSFSPopoutPanelManager
return sourceImage;
}
public static Bitmap GetExpandButtonImage(int windowHeight)
{
var image = new Bitmap(FileManager.LoadAsStream(FilePathType.PreprocessingData, "separation_button.png"));
double template_image_ratio = Convert.ToDouble(windowHeight) / 1440; // expand button image was created on 1440p resolution
return ImageOperation.ResizeImage(image, Convert.ToInt32(image.Width * template_image_ratio), Convert.ToInt32(image.Height * template_image_ratio));
}
}
}

View file

@ -37,7 +37,7 @@ namespace MSFSPopoutPanelManager
{
while (panelsToBeIdentified > 1) // Do not have to separate the last panel
{
var coordinate = AnalyzeMergedPopoutWindows(simulatorProcess.Handle, customPopout.Handle);
var coordinate = AnalyzeMergedPopoutWindows(customPopout.Handle);
if (!coordinate.IsEmpty)
SeparateUntitledPanel(customPopout.Handle, coordinate.X, coordinate.Y);
@ -56,7 +56,6 @@ namespace MSFSPopoutPanelManager
AnalyzePopoutWindows(simulatorProcess, profileId);
}
private WindowProcess GetProcessZero()
{
// Get process with PID of zero (PID zero launches all the popout windows for MSFS)
@ -170,110 +169,56 @@ namespace MSFSPopoutPanelManager
}
}
public Point AnalyzeMergedPopoutWindows(IntPtr flightSimHandle, IntPtr windowHandle)
public Point AnalyzeMergedPopoutWindows(IntPtr windowHandle)
{
var resolution = GetWindowResolution(flightSimHandle);
float EXHAUSTIVE_TEMPLATE_MATCHING_SIMILARITY_THRESHOLD = 0.86f;
int EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR = 1;
float EXHAUSTIVE_TEMPLATE_MATCHING_SIMILARITY_THRESHOLD = 0.9f;
var sourceImage = ImageOperation.TakeScreenShot(windowHandle, true);
var templateImage = ImageOperation.GetExpandButtonImage(sourceImage.Height);
// This is to speed up pattern matching and still balance accuracy
switch (resolution)
{
case FlightSimResolution.HD:
EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR = 2;
EXHAUSTIVE_TEMPLATE_MATCHING_SIMILARITY_THRESHOLD = 0.88f;
break;
case FlightSimResolution.QHD:
case FlightSimResolution.WQHD:
case FlightSimResolution.UHD:
EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR = 3;
EXHAUSTIVE_TEMPLATE_MATCHING_SIMILARITY_THRESHOLD = 0.85f;
break;
}
var panelsStartingTop = GetPanelsStartingTop(windowHandle, sourceImage);
var windowResolution = GetWindowResolution(flightSimHandle);
var templateFileName = $"separation_button_{windowResolution}.png";
var source = ImageOperation.ConvertToFormat(ImageOperation.TakeScreenShot(windowHandle, true), PixelFormat.Format24bppRgb);
var template = ImageOperation.ConvertToFormat(new Bitmap(FileManager.LoadAsStream(FilePathType.PreprocessingData, templateFileName)), PixelFormat.Format24bppRgb);
// Get the updated template image ratio based on how the initial window with all the popout panels in it are organized. This is used to resize the template image
var templateImageRatio = GetTemplateToPanelHeightRatio(windowHandle, source, windowResolution, 1);
if (templateImageRatio == -1)
{
if (panelsStartingTop > sourceImage.Height / 2) // if usually the last panel occupied the entire window with no white menubar
return Point.Empty;
}
// Resize the source and template image to speed up exhaustive template matching analysis
var sourceWidth = source.Width / EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR;
var sourceHeight = source.Height / EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR;
var templateWidth = template.Width * templateImageRatio / EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR;
var templateHeight = template.Height * templateImageRatio / EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR;
var panelsStartingLeft = GetPanelsStartingLeft(windowHandle, sourceImage, panelsStartingTop + 5);
var resizedSource = ImageOperation.ResizeImage(source, sourceWidth, sourceHeight);
resizedSource = ImageOperation.ConvertToFormat(resizedSource, PixelFormat.Format24bppRgb);
var templateImageRatios = GetExpandButtonHeightRatio(windowHandle, sourceImage, 1);
var resizedSource = ImageOperation.CropImage(sourceImage, 0, panelsStartingTop, sourceImage.Width, sourceImage.Height / 12); // add around 100px per 1440p resolution
resizedSource.Save(FileManager.GetFilePathByType(FilePathType.PreprocessingData) + "source.png");
var resizedTemplate = ImageOperation.ResizeImage(templateImage, templateImage.Width * templateImageRatios[0], templateImage.Height * templateImageRatios[0]);
var point = ImageAnalysis.ExhaustiveTemplateMatchAnalysisAsync(resizedSource, resizedTemplate, EXHAUSTIVE_TEMPLATE_MATCHING_SIMILARITY_THRESHOLD, panelsStartingTop, panelsStartingLeft);
var resizedTemplate = ImageOperation.ResizeImage(template, templateWidth, templateHeight);
var point = ImageAnalysis.ExhaustiveTemplateMatchAnalysis(resizedSource, resizedTemplate, EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR, EXHAUSTIVE_TEMPLATE_MATCHING_SIMILARITY_THRESHOLD);
if (point.IsEmpty)
{
template = ImageOperation.ConvertToFormat(new Bitmap(FileManager.LoadAsStream(FilePathType.PreprocessingData, templateFileName)), PixelFormat.Format24bppRgb);
templateImageRatio = GetTemplateToPanelHeightRatio(windowHandle, source, windowResolution, 2); // maybe there are 2 rows of panels in the merged pop out window
templateWidth = template.Width * templateImageRatio / EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR;
templateHeight = template.Height * templateImageRatio / EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR;
resizedTemplate = ImageOperation.ResizeImage(template, templateWidth, templateHeight);
point = ImageAnalysis.ExhaustiveTemplateMatchAnalysis(resizedSource, resizedTemplate, EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR, EXHAUSTIVE_TEMPLATE_MATCHING_SIMILARITY_THRESHOLD);
resizedTemplate = ImageOperation.ResizeImage(templateImage, templateImage.Width * templateImageRatios[1], templateImage.Height * templateImageRatios[1]);
point = ImageAnalysis.ExhaustiveTemplateMatchAnalysisAsync(resizedSource, resizedTemplate, EXHAUSTIVE_TEMPLATE_MATCHING_SIMILARITY_THRESHOLD, panelsStartingTop, panelsStartingLeft);
}
return point;
}
private FlightSimResolution GetWindowResolution(IntPtr windowHandle)
private List<double> GetExpandButtonHeightRatio(IntPtr windowHandle, Bitmap sourceImage, int numberOfRows, double percentFromLeft = 0.48)
{
var rect = new Rect();
PInvoke.GetClientRect(windowHandle, out rect);
var ratios = new List<double>();
switch (rect.Bottom)
{
case 1009:
case 1080:
return FlightSimResolution.HD;
case 1369:
case 1440:
return FlightSimResolution.QHD;
case 2089:
case 2160:
return FlightSimResolution.UHD;
default:
return FlightSimResolution.HD;
}
}
private float GetTemplateToPanelHeightRatio(IntPtr windowHandle, Bitmap sourceImage, FlightSimResolution windowResolution, int numberOfRows)
{
const int SW_MAXIMIZE = 3;
PInvoke.ShowWindow(windowHandle, SW_MAXIMIZE);
PInvoke.SetForegroundWindow(windowHandle);
Thread.Sleep(500);
Thread.Sleep(200);
Rect rect = new Rect();
PInvoke.GetClientRect(windowHandle, out rect);
// Get a snippet of 1 pixel wide vertical strip of windows. We will choose the strip left of center.
// This is to determine when the actual panel's vertical pixel starts in the window. This will allow accurate sizing of the template image
// This is to determine when the actual panel's vertical pixel starts in the window. This will allow accurate sizing of the expand button image
var clientWindowHeight = rect.Bottom - rect.Top;
var left = Convert.ToInt32((rect.Right - rect.Left) * 0.48); // look at around 48% from the left
var left = Convert.ToInt32((rect.Right - rect.Left) * percentFromLeft); // look at around 48% from the left
var top = sourceImage.Height - clientWindowHeight;
if (top < 0 || left < 0)
return -1;
// Using much faster image LockBits instead of GetPixel method
unsafe
{
@ -284,9 +229,62 @@ namespace MSFSPopoutPanelManager
int widthInBytes = stripData.Width * bytesPerPixel;
byte* ptrFirstPixel = (byte*)stripData.Scan0;
// Find the first white pixel and have at least 4 more white pixels in a row (the panel title bar)
const int WHITE_PIXEL_TO_COUNT = 4;
int whitePixelCount = 0;
// Find the first white pixel (the panel title bar)
for (int y = 0; y < heightInPixels; y++)
{
byte* currentLine = ptrFirstPixel + (y * stripData.Stride);
for (int x = 0; x < widthInBytes; x = x + bytesPerPixel)
{
int red = currentLine[x + 2];
int green = currentLine[x + 1];
int blue = currentLine[x];
if (red == 255 && green == 255 && blue == 255)
{
sourceImage.UnlockBits(stripData);
var unpopPanelSize = (clientWindowHeight - (y * 2)) / Convert.ToDouble(numberOfRows);
ratios.Add(unpopPanelSize / Convert.ToDouble(clientWindowHeight)); // 1 row of panel
ratios.Add(unpopPanelSize / 2 / Convert.ToDouble(clientWindowHeight)); // 2 rows of panel
return ratios;
}
}
}
sourceImage.UnlockBits(stripData);
}
return GetExpandButtonHeightRatio(windowHandle, sourceImage, numberOfRows, percentFromLeft - 0.01);
}
private int GetPanelsStartingTop(IntPtr windowHandle, Bitmap sourceImage, double percentFromLeft = 0.49)
{
const int SW_MAXIMIZE = 3;
PInvoke.ShowWindow(windowHandle, SW_MAXIMIZE);
PInvoke.SetForegroundWindow(windowHandle);
Thread.Sleep(250);
Rect rect = new Rect();
PInvoke.GetClientRect(windowHandle, out rect);
// Get a snippet of 1 pixel wide vertical strip of windows. We will choose the strip left of center.
// This is to determine when the actual panel's vertical pixel starts in the window. This will allow accurate sizing of the template image
var clientWindowHeight = rect.Bottom - rect.Top;
var left = Convert.ToInt32((rect.Right - rect.Left) * percentFromLeft); // look at around 49% from the left
var top = sourceImage.Height - clientWindowHeight;
if (top < 0 || left < 0)
return -1;
unsafe
{
var stripData = sourceImage.LockBits(new Rectangle(left, top, 1, clientWindowHeight), ImageLockMode.ReadWrite, sourceImage.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(stripData.PixelFormat) / 8;
int heightInPixels = stripData.Height;
int widthInBytes = stripData.Width * bytesPerPixel;
byte* ptrFirstPixel = (byte*)stripData.Scan0;
for (int y = 0; y < heightInPixels; y++)
{
@ -299,19 +297,48 @@ namespace MSFSPopoutPanelManager
if (red == 255 && green == 255 && blue == 255)
{
whitePixelCount++;
if (whitePixelCount == WHITE_PIXEL_TO_COUNT)
{
sourceImage.UnlockBits(stripData);
var unpopPanelSize = (clientWindowHeight - (y) * 2) / Convert.ToSingle(numberOfRows);
var currentRatio = unpopPanelSize / Convert.ToSingle(clientWindowHeight);
return currentRatio;
}
sourceImage.UnlockBits(stripData);
return y + top;
}
else
}
}
sourceImage.UnlockBits(stripData);
}
return GetPanelsStartingTop(windowHandle, sourceImage, percentFromLeft - 0.01);
}
private int GetPanelsStartingLeft(IntPtr windowHandle, Bitmap sourceImage, int top)
{
Rect rect = new Rect();
PInvoke.GetClientRect(windowHandle, out rect);
// Get a snippet of 1 pixel wide horizontal strip of windows
var clientWindowWidth = rect.Right - rect.Left;
unsafe
{
var stripData = sourceImage.LockBits(new Rectangle(0, top, clientWindowWidth, 1), ImageLockMode.ReadWrite, sourceImage.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(stripData.PixelFormat) / 8;
int widthInPixels = stripData.Width;
int heightInBytes = stripData.Height * bytesPerPixel;
byte* ptrFirstPixel = (byte*)stripData.Scan0;
for (int x = 0; x < widthInPixels; x++)
{
byte* currentLine = ptrFirstPixel - (x * bytesPerPixel);
for (int y = 0; y < heightInBytes; y = y + bytesPerPixel)
{
int red = currentLine[y + 2];
int green = currentLine[y + 1];
int blue = currentLine[y];
if (red == 255 && green == 255 && blue == 255)
{
whitePixelCount = 0;
sourceImage.UnlockBits(stripData);
return sourceImage.Width - x;
}
}
}
@ -331,11 +358,13 @@ namespace MSFSPopoutPanelManager
Cursor.Position = new Point(point.X, point.Y);
// Wait for mouse to get into position
Thread.Sleep(1000);
Thread.Sleep(500);
PInvoke.mouse_event(MOUSEEVENTF_LEFTDOWN, point.X, point.Y, 0, 0);
Thread.Sleep(200);
PInvoke.mouse_event(MOUSEEVENTF_LEFTUP, point.X, point.Y, 0, 0);
Cursor.Position = new Point(point.X + 50, point.Y + 50);
}
private void AnalyzePopoutWindows(WindowProcess simulatorProcess, int profileId)
@ -374,7 +403,7 @@ namespace MSFSPopoutPanelManager
Thread.Sleep(300); // ** this delay is important to allow the window to go into focus before screenshot is taken
var srcImage = ImageOperation.ConvertToFormat(ImageOperation.TakeScreenShot(popout.Handle, false), PixelFormat.Format24bppRgb);
var srcImage = ImageOperation.TakeScreenShot(popout.Handle, false);
var srcImageBytes = ImageOperation.ImageToByte(srcImage);
Parallel.ForEach(templates, new ParallelOptions { MaxDegreeOfParallelism = Convert.ToInt32(Math.Ceiling((Environment.ProcessorCount * 0.75) * 1.0)) }, template =>

View file

@ -150,4 +150,4 @@ namespace MSFSPopoutPanelManager
DrawPanelLocationOverlay();
}
}
}
}

View file

@ -188,7 +188,10 @@ namespace MSFSPopoutPanelManager
else
Logger.LogStatus("No panel has been identified.");
ApplyPanelSettings();
if(hasExistingData)
ApplyPanelSettings();
PInvoke.SetForegroundWindow(_appForm.Handle);
}
public void ApplyPanelSettings()
@ -225,6 +228,8 @@ namespace MSFSPopoutPanelManager
Logger.LogStatus("Please move the newly identified panels to their desire locations. Once everything is perfect, click 'Save Settings' and these settings will be used in future flights.");
else
Logger.LogStatus("No panel has been found.");
PInvoke.SetForegroundWindow(_appForm.Handle);
}
public void SavePanelSettings()

View file

@ -1,4 +1,6 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Windows.Forms;
namespace MSFSPopoutPanelManager
@ -11,10 +13,28 @@ namespace MSFSPopoutPanelManager
[STAThread]
static void Main()
{
Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new StartupForm());
bool createNew;
using var mutex = new Mutex(true, typeof(Program).Namespace, out createNew);
if (createNew)
{
Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new StartupForm());
}
else
{
var current = Process.GetCurrentProcess();
foreach (var process in Process.GetProcessesByName(current.ProcessName))
{
if (process.Id == current.Id) continue;
PInvoke.SetForegroundWindow(process.MainWindowHandle);
break;
}
}
}
}
}

View file

@ -116,10 +116,9 @@ The user created plane profile and associate image recognition data are stored i
## Common Problem Resolution
- Unable to pop out windows correctly - the predefined pop out panel coordinate may not line up correctly or movement of mouse is interfering with pop out execution. Please try to reposition the screen into pop out coordinates. Or you can close and restart the application, close all the opened pop outs, and try the analysis again.
- Pop out windows are not recognized correctly - it is the current limitation of current implementation of image recognition algorithm. More sophisticated image recognition such as [SIFT](https://en.wikipedia.org/wiki/Scale-invariant_feature_transform) will be needed. This is to-do item on my list for future version of app. Also, the panel screen maybe blank which causes the image recognition engine to fail.
- Night time or different world location causes image recognition to fail - application has builtin redundancy image recognition data for this purpose (such as MFD recognition for G1000 where 75% of the screen is a map). But I may not have anticipate all the use cases yet. Please provide feedback and it will help me to improve the image recognition engine.
- Running on non-native monitor resolution does not work - for example running 1080p window resolution on 1440p monitor will not work because of image scaling and calculation issue. But in-game resolution scaling will not get affected. This issue can only be fixed when using scale invariant algorithm or more advanced algorithm.
- Pop out windows are not recognized correctly - it is limitation of current implementation of image recognition algorithm. More sophisticated image recognition such as [SIFT](https://en.wikipedia.org/wiki/Scale-invariant_feature_transform) will be needed. This is to-do item on my list for future version of app. Also, the panel screen maybe blank which causes the image recognition engine to fail.
- Night time or different world location causes image recognition to fail - application has builtin redundancy image recognition data for this purpose (such as MFD recognition for G1000 where 75% of the screen is a map). But I may not have anticipate all the use cases yet. Please provide feedback and it will help me to improve the image recognition engine.
## Author
Stanley Kwok

View file

@ -42,6 +42,7 @@ namespace MSFSPopoutPanelManager
this.darkLabel2 = new DarkUI.Controls.DarkLabel();
this.checkBoxMinimizeToTray = new DarkUI.Controls.DarkCheckBox();
this.lblVersion = new DarkUI.Controls.DarkLabel();
this.checkBoxAutoStart = new DarkUI.Controls.DarkCheckBox();
this.panelStatus.SuspendLayout();
this.SuspendLayout();
//
@ -72,7 +73,7 @@ namespace MSFSPopoutPanelManager
this.labelMsfsRunning.AutoSize = true;
this.labelMsfsRunning.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.labelMsfsRunning.ForeColor = System.Drawing.Color.Red;
this.labelMsfsRunning.Location = new System.Drawing.Point(704, 553);
this.labelMsfsRunning.Location = new System.Drawing.Point(704, 546);
this.labelMsfsRunning.Name = "labelMsfsRunning";
this.labelMsfsRunning.RightToLeft = System.Windows.Forms.RightToLeft.Yes;
this.labelMsfsRunning.Size = new System.Drawing.Size(143, 20);
@ -147,7 +148,7 @@ namespace MSFSPopoutPanelManager
//
this.checkBoxMinimizeToTray.AutoSize = true;
this.checkBoxMinimizeToTray.Font = new System.Drawing.Font("Segoe UI", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.checkBoxMinimizeToTray.Location = new System.Drawing.Point(13, 552);
this.checkBoxMinimizeToTray.Location = new System.Drawing.Point(13, 546);
this.checkBoxMinimizeToTray.Name = "checkBoxMinimizeToTray";
this.checkBoxMinimizeToTray.Size = new System.Drawing.Size(189, 24);
this.checkBoxMinimizeToTray.TabIndex = 23;
@ -157,17 +158,29 @@ namespace MSFSPopoutPanelManager
//
this.lblVersion.AutoSize = true;
this.lblVersion.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(220)))), ((int)(((byte)(220)))), ((int)(((byte)(220)))));
this.lblVersion.Location = new System.Drawing.Point(383, 561);
this.lblVersion.Location = new System.Drawing.Point(383, 572);
this.lblVersion.Name = "lblVersion";
this.lblVersion.Size = new System.Drawing.Size(48, 15);
this.lblVersion.TabIndex = 24;
this.lblVersion.Text = "Version ";
//
// checkBoxAutoStart
//
this.checkBoxAutoStart.AutoSize = true;
this.checkBoxAutoStart.Font = new System.Drawing.Font("Segoe UI", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.checkBoxAutoStart.Location = new System.Drawing.Point(222, 546);
this.checkBoxAutoStart.Name = "checkBoxAutoStart";
this.checkBoxAutoStart.Size = new System.Drawing.Size(95, 24);
this.checkBoxAutoStart.TabIndex = 25;
this.checkBoxAutoStart.Text = "Auto Start";
this.checkBoxAutoStart.CheckedChanged += new System.EventHandler(this.checkBoxAutoStart_CheckedChanged);
//
// StartupForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(859, 583);
this.ClientSize = new System.Drawing.Size(859, 591);
this.Controls.Add(this.checkBoxAutoStart);
this.Controls.Add(this.lblVersion);
this.Controls.Add(this.checkBoxMinimizeToTray);
this.Controls.Add(this.darkLabel2);
@ -205,5 +218,6 @@ namespace MSFSPopoutPanelManager
private DarkUI.Controls.DarkCheckBox checkBoxMinimizeToTray;
private DarkUI.Controls.DarkLabel lblVersion;
private DarkUI.Controls.DarkLabel darkLabel3;
private DarkUI.Controls.DarkCheckBox checkBoxAutoStart;
}
}

View file

@ -1,6 +1,5 @@
using DarkUI.Forms;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Threading;
@ -41,6 +40,8 @@ namespace MSFSPopoutPanelManager
_panelManager.OnAnalysisCompleted += (source, e) => { _ucPanelSelection.Visible = false; _ucApplySettings.Visible = true; };
_panelManager.CheckSimulatorStarted();
checkBoxAutoStart.Checked = Autostart.CheckIsAutoStart();
}
private void Logger_OnStatusLogged(object sender, EventArgs<StatusMessage> e)
@ -101,6 +102,14 @@ namespace MSFSPopoutPanelManager
Process.Start(new ProcessStartInfo("https://github.com/hawkeye-stan/msfs-popout-panel-manager") { UseShellExecute = true });
}
private void checkBoxAutoStart_CheckedChanged(object sender, EventArgs e)
{
if (checkBoxAutoStart.Checked)
Autostart.Activate();
else
Autostart.Deactivate();
}
}
}

View file

@ -1,6 +1,14 @@
# Version History
<hr/>
## Vesion 2.2.0.0
* Disabled ability to launch multiple instances of the application.
* Added autostart feature when MSFS starts. The application will create or modify exe.xml. A backup copy of exe.xml will be created.
* Added better support for 4K display resolution and non-standard display resolution.
* Windows OS display resolution and in-game display resolution no longer have to match.
* Improved panel pop out separation accuracy and performance.
* Updated application packaging to single file executable to reduce file clutter.
## Vesion 2.1.1.0
* Fixed panel separation issue for super ultrawide monitor (for example: 3840x1080)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB