Skip to content

Commit ec06ddf

Browse files
committed
fix missing icon for store apps
1 parent 7fbada2 commit ec06ddf

File tree

3 files changed

+262
-40
lines changed

3 files changed

+262
-40
lines changed

AppGroup/AppGroup.csproj

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,42 @@
2929
<Content Include="Assets\SplashScreen.scale-200.png" />
3030
<Content Include="Assets\LockScreenLogo.scale-200.png" />
3131
<Content Include="Assets\Square150x150Logo.scale-200.png" />
32-
<Content Include="Assets\Square44x44Logo.scale-200.png">
33-
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
34-
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
35-
</Content>
32+
<Content Include="Assets\Square44x44Logo.scale-200.png"/>
3633
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
3734
<Content Include="Assets\StoreLogo.png" />
3835
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
39-
<Content Include="AppGroup.ico" />
40-
<Content Include="EditGroup.ico" />
41-
<Content Include="AppGroup.png" />
42-
<Content Include="default_preview.png" />
43-
<Content Include="Assets\coffee_dark.png" />
44-
<Content Include="Assets\coffee_light.png" />
45-
<Content Include="Assets\github_dark.png" />
46-
<Content Include="Assets\github_light.png" />
36+
<Content Include="AppGroup.ico" >
37+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
38+
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
39+
</Content>
40+
<Content Include="EditGroup.ico" >
41+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
42+
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
43+
</Content>
44+
<Content Include="AppGroup.png" >
45+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
46+
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
47+
</Content>
48+
<Content Include="default_preview.png" >
49+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
50+
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
51+
</Content>
52+
<Content Include="Assets\coffee_dark.png" >
53+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
54+
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
55+
</Content>
56+
<Content Include="Assets\coffee_light.png">
57+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
58+
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
59+
</Content>
60+
<Content Include="Assets\github_dark.png" >
61+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
62+
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
63+
</Content>
64+
<Content Include="Assets\github_light.png" >
65+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
66+
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
67+
</Content>
4768
</ItemGroup>
4869

4970
<ItemGroup>

AppGroup/IconHelper.cs

Lines changed: 194 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using System.Text;
1414
using System.Threading;
1515
using System.Threading.Tasks;
16+
using System.Xml;
1617
using Windows.Storage;
1718
using Windows.Storage.FileProperties;
1819
using Windows.Storage.Streams;
@@ -40,9 +41,146 @@ public static string FindOrigIcon(string icoFilePath) {
4041
}
4142

4243

44+
private static async Task<Bitmap> ExtractWindowsAppIconAsync(string shortcutPath, string outputDirectory) {
45+
try {
46+
// Get the shortcut target using Shell COM objects
47+
Type shellType = Type.GetTypeFromProgID("Shell.Application");
48+
if (shellType == null) return null;
49+
50+
dynamic shell = Activator.CreateInstance(shellType);
51+
dynamic folder = shell.Namespace(Path.GetDirectoryName(shortcutPath));
52+
dynamic shortcutItem = folder.ParseName(Path.GetFileName(shortcutPath));
53+
54+
// Find the "Link target" property
55+
string linkTarget = null;
56+
for (int i = 0; i < 500; i++) {
57+
string propertyName = folder.GetDetailsOf(null, i);
58+
if (propertyName == "Link target") {
59+
linkTarget = folder.GetDetailsOf(shortcutItem, i);
60+
break;
61+
}
62+
}
63+
64+
if (string.IsNullOrEmpty(linkTarget)) return null;
65+
66+
// Extract the app name from the link target (remove everything after the first "_")
67+
string appName = System.Text.RegularExpressions.Regex.Replace(linkTarget, "_.*$", "");
68+
if (string.IsNullOrEmpty(appName)) return null;
69+
70+
// Use Windows Runtime API to find the package
71+
Windows.Management.Deployment.PackageManager packageManager = new Windows.Management.Deployment.PackageManager();
72+
IEnumerable<Windows.ApplicationModel.Package> packages = packageManager.FindPackagesForUser("");
73+
74+
// Find the package that matches the app name
75+
Windows.ApplicationModel.Package appPackage = packages.FirstOrDefault(p => p.Id.Name.StartsWith(appName, StringComparison.OrdinalIgnoreCase));
76+
if (appPackage == null) return null;
77+
78+
string installPath = appPackage.InstalledLocation.Path;
79+
string manifestPath = Path.Combine(installPath, "AppxManifest.xml");
80+
81+
if (!File.Exists(manifestPath)) return null;
82+
83+
// Load and parse the manifest XML
84+
XmlDocument manifest = new XmlDocument();
85+
manifest.Load(manifestPath);
86+
87+
// Create namespace manager
88+
XmlNamespaceManager nsManager = new XmlNamespaceManager(manifest.NameTable);
89+
nsManager.AddNamespace("ns", "http://schemas.microsoft.com/appx/manifest/foundation/windows10");
90+
91+
// Get logo path from manifest
92+
XmlNode logoNode = manifest.SelectSingleNode("/ns:Package/ns:Properties/ns:Logo", nsManager);
93+
if (logoNode == null) return null;
94+
95+
string logoPath = logoNode.InnerText;
96+
string logoDir = Path.Combine(installPath, Path.GetDirectoryName(logoPath));
97+
98+
if (!Directory.Exists(logoDir)) return null;
99+
100+
string[] logoPatterns = new[] {
101+
102+
"*StoreLogo*.png",
103+
104+
};
105+
106+
string highestResLogoPath = null;
107+
long highestSize = 0;
43108

109+
foreach (string pattern in logoPatterns) {
110+
foreach (string file in Directory.GetFiles(logoDir, pattern, SearchOption.AllDirectories)) {
111+
FileInfo fileInfo = new FileInfo(file);
112+
if (fileInfo.Length > highestSize) {
113+
highestSize = fileInfo.Length;
114+
highestResLogoPath = file;
115+
}
116+
}
117+
118+
if (highestResLogoPath != null) break;
119+
}
120+
121+
if (string.IsNullOrEmpty(highestResLogoPath) || !File.Exists(highestResLogoPath)) return null;
122+
123+
// Load the image and resize/crop it to 200x200
124+
using (FileStream stream = new FileStream(highestResLogoPath, FileMode.Open, FileAccess.Read)) {
125+
using (var originalBitmap = new Bitmap(stream)) {
126+
// Create a square bitmap of 200x200
127+
var resizedIcon = ResizeAndCropImageToSquare(originalBitmap, 200);
128+
return resizedIcon;
129+
}
130+
}
131+
}
132+
catch (Exception ex) {
133+
Debug.WriteLine($"Error extracting Windows app icon: {ex.Message}");
134+
return null;
135+
}
136+
}
44137

138+
/// <summary>
139+
/// Resizes and crops an image to a square with the specified size
140+
/// </summary>
141+
private static Bitmap ResizeAndCropImageToSquare(Bitmap originalImage, int size, float zoomFactor = 1.1f) {
142+
try {
143+
// Create a new square bitmap
144+
Bitmap resizedImage = new Bitmap(size, size);
145+
146+
// Calculate dimensions for maintaining aspect ratio
147+
int sourceWidth = originalImage.Width;
148+
int sourceHeight = originalImage.Height;
149+
150+
// Find the smallest dimension and calculate the crop area
151+
int cropSize = Math.Min(sourceWidth, sourceHeight);
152+
153+
// Apply zoom factor (smaller crop size = more zoom)
154+
cropSize = (int)(cropSize / zoomFactor);
155+
156+
// Center the cropping rectangle
157+
int cropX = (sourceWidth - cropSize) / 2;
158+
int cropY = (sourceHeight - cropSize) / 2;
159+
160+
// Create a graphics object to perform the resize
161+
using (Graphics g = Graphics.FromImage(resizedImage)) {
162+
// Set high quality mode for better results
163+
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
164+
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
165+
g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
166+
g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
167+
168+
// Draw the centered and cropped image to maintain aspect ratio
169+
g.DrawImage(originalImage,
170+
new Rectangle(0, 0, size, size),
171+
new Rectangle(cropX, cropY, cropSize, cropSize),
172+
GraphicsUnit.Pixel);
173+
}
174+
175+
return resizedImage;
176+
}
177+
catch (Exception ex) {
178+
Debug.WriteLine($"Error resizing image: {ex.Message}");
179+
return null;
180+
}
181+
}
45182

183+
// Also modify the main ExtractIconAndSaveAsync method to use this resize function for all icons
46184
public static async Task<string> ExtractIconAndSaveAsync(string filePath, string outputDirectory, TimeSpan? timeout = null) {
47185
timeout ??= TimeSpan.FromSeconds(3);
48186
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) {
@@ -54,45 +192,63 @@ public static async Task<string> ExtractIconAndSaveAsync(string filePath, string
54192
return await Task.Run(async () => {
55193
try {
56194
Bitmap iconBitmap = null;
195+
string appName = string.Empty;
57196
if (Path.GetExtension(filePath).ToLower() == ".lnk") {
58-
dynamic shell = Microsoft.VisualBasic.Interaction.CreateObject("WScript.Shell");
59-
dynamic shortcut = shell.CreateShortcut(filePath);
60-
string iconPath = shortcut.IconLocation;
61-
string targetPath = shortcut.TargetPath;
62-
if (!string.IsNullOrEmpty(iconPath) && iconPath != ",") {
63-
string[] iconInfo = iconPath.Split(',');
64-
string actualIconPath = iconInfo[0].Trim();
65-
int iconIndex = iconInfo.Length > 1 ? int.Parse(iconInfo[1].Trim()) : 0;
66-
if (File.Exists(actualIconPath)) {
67-
iconBitmap = ExtractSpecificIcon(actualIconPath, iconIndex);
197+
// Try to extract Windows app shortcut icon first
198+
iconBitmap = await ExtractWindowsAppIconAsync(filePath, outputDirectory);
199+
200+
201+
// If Windows app icon extraction failed, fall back to the existing method
202+
if (iconBitmap == null) {
203+
dynamic shell = Microsoft.VisualBasic.Interaction.CreateObject("WScript.Shell");
204+
dynamic shortcut = shell.CreateShortcut(filePath);
205+
string iconPath = shortcut.IconLocation;
206+
string targetPath = shortcut.TargetPath;
207+
if (!string.IsNullOrEmpty(iconPath) && iconPath != ",") {
208+
string[] iconInfo = iconPath.Split(',');
209+
string actualIconPath = iconInfo[0].Trim();
210+
int iconIndex = iconInfo.Length > 1 ? int.Parse(iconInfo[1].Trim()) : 0;
211+
if (File.Exists(actualIconPath)) {
212+
iconBitmap = ExtractSpecificIcon(actualIconPath, iconIndex);
213+
}
214+
}
215+
if (iconBitmap == null && !string.IsNullOrEmpty(targetPath) && File.Exists(targetPath)) {
216+
iconBitmap = ExtractIconWithoutArrow(targetPath);
217+
}
218+
219+
//Use the Icon with arrow as fallback
220+
if (iconBitmap == null) {
221+
Icon icon = Icon.ExtractAssociatedIcon(filePath);
222+
iconBitmap = icon.ToBitmap();
68223
}
69-
}
70-
if (iconBitmap == null && !string.IsNullOrEmpty(targetPath) && File.Exists(targetPath)) {
71-
iconBitmap = ExtractIconWithoutArrow(targetPath);
72224
}
73225
}
74226
else {
75227
iconBitmap = ExtractIconWithoutArrow(filePath);
228+
76229
}
77230
if (iconBitmap == null) {
78231
Debug.WriteLine($"No icon found for file: {filePath}");
79232
return null;
80233
}
81-
Directory.CreateDirectory(outputDirectory);
82234

83-
string iconFileName = GenerateUniqueIconFileName(filePath, iconBitmap);
84-
string iconFilePath = Path.Combine(outputDirectory, iconFileName);
235+
using (Bitmap resizedIcon = ResizeAndCropImageToSquare(iconBitmap, 200)) {
236+
Directory.CreateDirectory(outputDirectory);
237+
string iconFileName = GenerateUniqueIconFileName(filePath, resizedIcon);
238+
string iconFilePath = Path.Combine(outputDirectory, iconFileName);
85239

86-
if (File.Exists(iconFilePath)) {
87-
return iconFilePath;
88-
}
240+
if (File.Exists(iconFilePath)) {
241+
return iconFilePath;
242+
}
243+
244+
using (var stream = new FileStream(iconFilePath, FileMode.Create)) {
245+
cancellationTokenSource.Token.ThrowIfCancellationRequested();
246+
resizedIcon.Save(stream, ImageFormat.Png);
247+
}
89248

90-
using (var stream = new FileStream(iconFilePath, FileMode.Create)) {
91-
cancellationTokenSource.Token.ThrowIfCancellationRequested();
92-
iconBitmap.Save(stream, ImageFormat.Png);
249+
Debug.WriteLine($"Icon saved to: {iconFilePath}");
250+
return iconFilePath;
93251
}
94-
Debug.WriteLine($"Icon saved to: {iconFilePath}");
95-
return iconFilePath;
96252
}
97253
catch (OperationCanceledException) {
98254
Debug.WriteLine($"Icon extraction timed out for: {filePath}");
@@ -106,6 +262,10 @@ public static async Task<string> ExtractIconAndSaveAsync(string filePath, string
106262
}
107263
}
108264

265+
266+
267+
268+
109269
private static string GenerateUniqueIconFileName(string filePath, Bitmap iconBitmap) {
110270
using (var md5 = System.Security.Cryptography.MD5.Create()) {
111271
byte[] filePathBytes = System.Text.Encoding.UTF8.GetBytes(filePath);
@@ -349,7 +509,7 @@ public static async Task<BitmapImage> ExtractIconFromFileAsync(string filePath,
349509
}
350510

351511

352-
public static bool ConvertToIco(string sourcePath, string icoFilePath) {
512+
public static bool ConvertToIco(string sourcePath, string icoFilePath) {
353513
if (string.IsNullOrEmpty(sourcePath) || string.IsNullOrEmpty(icoFilePath)) {
354514
Debug.WriteLine("Invalid source or destination path.");
355515
return false;
@@ -476,6 +636,10 @@ public async Task<string> CreateGridIconAsync(
476636
System.Drawing.Bitmap iconBitmap = null;
477637

478638
if (Path.GetExtension(filePath).ToLower() == ".lnk") {
639+
iconBitmap = await ExtractWindowsAppIconAsync(filePath, tempFolder);
640+
641+
642+
479643
dynamic shell = Microsoft.VisualBasic.Interaction.CreateObject("WScript.Shell");
480644
dynamic shortcut = shell.CreateShortcut(filePath);
481645

@@ -495,6 +659,11 @@ public async Task<string> CreateGridIconAsync(
495659
if (iconBitmap == null && !string.IsNullOrEmpty(targetPath) && File.Exists(targetPath)) {
496660
iconBitmap = ExtractIconWithoutArrow(targetPath);
497661
}
662+
//Use the Icon with arrow as fallback
663+
if (iconBitmap == null) {
664+
Icon icon = Icon.ExtractAssociatedIcon(filePath);
665+
iconBitmap = icon.ToBitmap();
666+
}
498667
}
499668
else {
500669
iconBitmap = ExtractIconWithoutArrow(filePath);

AppGroup/PopupWindow.xaml.cs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -595,13 +595,45 @@ private OverlappedPresenter GetAppWindowAndPresenter() {
595595
return _apw.Presenter as OverlappedPresenter;
596596
}
597597

598+
private bool _isClosingInProgress = false;
599+
598600
private void Window_Activated(object sender, WindowActivatedEventArgs e) {
599-
if (e.WindowActivationState == WindowActivationState.Deactivated) {
600-
this.Close();
601+
if (e.WindowActivationState == WindowActivationState.Deactivated && !_isClosingInProgress) {
602+
_isClosingInProgress = true;
601603

602-
}
604+
try {
605+
this.Activated -= Window_Activated;
606+
607+
if (gridView != null) {
608+
try {
609+
gridView.RightTapped -= GridView_RightTapped;
610+
gridView.DragItemsCompleted -= GridView_DragItemsCompleted;
611+
}
612+
catch (Exception ex) {
613+
Debug.WriteLine($"Error detaching gridView events: {ex.Message}");
614+
}
615+
}
603616

617+
DispatcherQueue.TryEnqueue(() => {
618+
try {
619+
this.Close();
620+
}
621+
catch (Exception ex) {
622+
Debug.WriteLine($"Error closing window: {ex.Message}");
623+
}
624+
});
625+
}
626+
catch (Exception ex) {
627+
Debug.WriteLine($"Error in Window_Activated: {ex.Message}");
628+
try {
629+
this.Close();
630+
}
631+
catch {
632+
}
633+
}
634+
}
604635
}
636+
605637
public static class NativeMethods {
606638
[DllImport("user32.dll")]
607639
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

0 commit comments

Comments
 (0)