Update: You can download a working sample here.
That’s a long title. That’s because if you take any part away from it it becomes an easy task. For example, pinning programmatically on Windows 7 has a solution here. But that method doesn’t work on Windows 10. Yet. This is the relevant code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] internal static extern IntPtr LoadLibrary(string lpLibFileName); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] internal static extern int LoadString(IntPtr hInstance, uint wID, StringBuilder lpBuffer, int nBufferMax); public static bool PinUnpinTaskbar(string filePath, bool pin) { if (!File.Exists(filePath)) throw new FileNotFoundException(filePath); int MAX_PATH = 255; var actionIndex = pin ? 5386 : 5387; // 5386 is the DLL index for"Pin to Tas&kbar", ref. http://www.win7dll.info/shell32_dll.html //uncomment the following line to pin to start instead //actionIndex = pin ? 51201 : 51394; StringBuilder szPinToStartLocalized = new StringBuilder(MAX_PATH); IntPtr hShell32 = LoadLibrary("Shell32.dll"); LoadString(hShell32, (uint)actionIndex, szPinToStartLocalized, MAX_PATH); string localizedVerb = szPinToStartLocalized.ToString(); string path = Path.GetDirectoryName(filePath); string fileName = Path.GetFileName(filePath); // create the shell application object dynamic shellApplication = Activator.CreateInstance(Type.GetTypeFromProgID("Shell.Application")); dynamic directory = shellApplication.NameSpace(path); dynamic link = directory.ParseName(fileName); dynamic verbs = link.Verbs(); for (int i = 0; i < verbs.Count(); i++) { dynamic verb = verbs.Item(i); if (verb.Name.Equals(localizedVerb)) { verb.DoIt(); return true; } } return false; } |
Also, pinning a local program is much easier than pinning one on the network, which isn’t officially allowed. The usual workaround is pinning another program to the taskbar and then changing the shortcut created in C:\Users\Alexandre\AppData\Roaming\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar. But if your program uses a Jump List, it will not be displayed on the pinned button, it will appear in a new button, and will be gone when you close the app, which is pretty lame. You can solve that by copying the network program locally, pinning it, and then changing the path to the network. Not very practical though, and definitely not programmatic.
So how can it be done you are asking? In this post I’ll outline how to pin on windows 10 and in part 2 I will show how to pin a program on the network with a working Jump List. I will be posting the code for this soon.
Pinning to the Taskbar programmatically on Windows 10
The solution to pin to the taskbar in windows 7 involved listing the verbs on the shell COM object, and invoking the one with the title ‘Pin to Taskbar’. But in windows 10, all the verbs that appear when right clicking the file in explorer show up in the COM object, except for the ‘Pin to Taskbar’ verb. It’s obviously the same object that’s providing the list, but for some reason one of the items only appears in explorer. My first attempt was renaming my program to explorer.exe. I thought maybe it’s just a simple check. Turns out, it is just a simple check. The program doesn’t even have to be in %windir%. Renaming your app to explorer.exe causes the missing verb to appear, and allows any program to be pinned programatically. You could even write a separate program called explorer.exe and leave it alongside your main app and call it to do the pinning. But who wants to rename their program? And who wants to leave more executables lying around with their app? Besides, what’s the fun in that?
So after some poking around, I discovered where the name of the app is stored in the process memory, and by changing that value before the first time you invoke the shell object, you can fool it into thinking your app is explorer.exe. And you can do that in C# using Marshall.Write, in the safety of safe code. You can skip right to the end of the post for the code, or you can read the explanation below.
Finding the PEB in C#
The first step is finding the location of the PEB. The PEB is the structure that contains all the information about the process that is running, including environment variables, modules loaded and, yes, the name of the image that is running. There is an undocumented function in ntdll.dll called NtQueryInformationProcess which will give us the base address of the PEB. This function might be changed in the future, but for windows 10 we should be safe. After that we navigate the structure using different offsets for 32 and 64-bit processes to find the Image Path. (I only tested this on 64-bit so if it doesn’t work on 32 bit the offsets might be wrong) and use the Marshall functions to change the value. After that we can use the old method of invoking the verbs.
I will write another post soon showing how to pin a program on the network, so be sure check for updates!
Using the code
This is how you use the code:
1 2 3 4 5 6 7 8 9 |
try { ChangeImagePathName("explorer.exe"); PinUnpinTaskbar(tempFilePath, true); } finally { RestoreImagePathName(); } |
And this is the supporting code. Thanks to the Process Hacker v1 project for the code to find the base address of the PEB. I removed a few enums for brevity. Drop it in a static class for the Increment function to work.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
static string originalImagePathName; static int unicodeSize = IntPtr.Size * 2; static void GetPointers(out IntPtr imageOffset, out IntPtr imageBuffer) { IntPtr pebBaseAddress = GetBasicInformation().PebBaseAddress; var processParameters = Marshal.ReadIntPtr(pebBaseAddress, 4 * IntPtr.Size); imageOffset = processParameters.Increment(4 * 4 + 5 * IntPtr.Size + unicodeSize + IntPtr.Size + unicodeSize); imageBuffer = Marshal.ReadIntPtr(imageOffset, IntPtr.Size); } internal static void ChangeImagePathName(string newFileName) { IntPtr imageOffset, imageBuffer; GetPointers(out imageOffset, out imageBuffer); //Read original data var imageLen = Marshal.ReadInt16(imageOffset); originalImagePathName = Marshal.PtrToStringUni(imageBuffer, imageLen / 2); var newImagePathName = Path.Combine(Path.GetDirectoryName(originalImagePathName), newFileName); if (newImagePathName.Length > originalImagePathName.Length) throw new Exception("new ImagePathName cannot be longer than the original one"); //Write the string, char by char var ptr = imageBuffer; foreach(var unicodeChar in newImagePathName) { Marshal.WriteInt16(ptr, unicodeChar); ptr = ptr.Increment(2); } Marshal.WriteInt16(ptr, 0); //Write the new length Marshal.WriteInt16(imageOffset, (short) (newImagePathName.Length * 2)); } internal static void RestoreImagePathName() { IntPtr imageOffset, ptr; GetPointers(out imageOffset, out ptr); foreach (var unicodeChar in originalImagePathName) { Marshal.WriteInt16(ptr, unicodeChar); ptr = ptr.Increment(2); } Marshal.WriteInt16(ptr, 0); Marshal.WriteInt16(imageOffset, (short)(originalImagePathName.Length * 2)); } public static ProcessBasicInformation GetBasicInformation() { uint status; ProcessBasicInformation pbi; int retLen; var handle = System.Diagnostics.Process.GetCurrentProcess().Handle; if ((status = NtQueryInformationProcess(handle, 0, out pbi, Marshal.SizeOf(typeof(ProcessBasicInformation)), out retLen)) >= 0xc0000000) throw new Exception("Windows exception. status=" + status); return pbi; } [DllImport("ntdll.dll")] public static extern uint NtQueryInformationProcess( [In] IntPtr ProcessHandle, [In] int ProcessInformationClass, [Out] out ProcessBasicInformation ProcessInformation, [In] int ProcessInformationLength, [Out] [Optional] out int ReturnLength ); public static IntPtr Increment(this IntPtr ptr, int value) { unchecked { if (IntPtr.Size == sizeof(Int32)) return new IntPtr(ptr.ToInt32() + value); else return new IntPtr(ptr.ToInt64() + value); } } [StructLayout(LayoutKind.Sequential)] public struct ProcessBasicInformation { public uint ExitStatus; public IntPtr PebBaseAddress; public IntPtr AffinityMask; public int BasePriority; public IntPtr UniqueProcessId; public IntPtr InheritedFromUniqueProcessId; } |
Nice, is there a way to do the same for the start menu?
Thanks
Yes there is. In the line that starts with
var actionIndex =
, instead of5386 : 5387
use51201 : 51394
. Note that in this case you don’t have to callChangeImagePathName
, the verb will appear normally.It seems that unpinning is not working?
PinUnpinTaskbar(tempFilePath, false);
Can you confirm?
Thanks
I can confirm that. To fix, go to project options->Build and uncheck ‘Prefer 32-bit’. I have no idea why, but that seems to do it. I updated the sample with this, and now it supports a /unpin option. Thanks for the heads up!
Thanks for your work Alex, this is excellent!
Would you be willing to offer a pre-compiled executable for us non-devs?
Thanks! In part 2 I have provided a compiled version, which also pins programs on the network.
I see that you say change the actionIndex and do not call ChangeImagePathName, in order to Pin to start. Making these modifications, the code hits verb.DoIt() correctly but does not do anything. Can you verify that you can pin to start?
Hi nick, I’m able to pin to start. It gets pinned as a tile on the right side of the menu, under all the other tiles. Try clicking on the file in explorer and choosing Pin to start, the same thing should happen. Are you using windows 10?
Im using Windows 10 version 10.0.1 Can you post the code for pin to start. I think I made the correct changes. I am matching the verbs (pin to start) and calling DoIt, no errors but no results
Just uncomment line 13 from the first listing to pin to the start menu.
What happens when you right click the same file in explorer and select pin to start? The same thing should happen. Are you using classic shell?
Im using .net 3.5
public static bool PinUnpinTaskbar(string filePath, bool pin, bool isStartMenu, OsVersion version)
{
if (!File.Exists(filePath)) throw new FileNotFoundException(filePath);
int MAX_PATH = 255;
var actionIndex = pin ? 5386 : 5387; // 5386 is the DLL index for”Pin to Tas&kbar”, ref. http://www.win7dll.info/shell32_dll.html
if (isStartMenu)
{
if (version == OsVersion.Win10)
{
actionIndex = pin ? 51201 : 51394;
}
else
{
actionIndex = pin ? 5381 : 5382;
}
}
StringBuilder szPinToStartLocalized = new StringBuilder(MAX_PATH);
IntPtr hShell32 = LoadLibrary(“Shell32.dll”);
LoadString(hShell32, (uint)actionIndex, szPinToStartLocalized, MAX_PATH);
string localizedVerb = szPinToStartLocalized.ToString();
localizedVerb = localizedVerb.Replace(@”&”, string.Empty).ToLower();
Shell shellApplication = new ShellClass();
string path = Path.GetDirectoryName(filePath);
string fileName = Path.GetFileName(filePath);
//Folder directory = shellApplication.NameSpace(path);
Folder directory = GetShell32NameSpaceFolder(path);
FolderItem link = directory.ParseName(fileName);
FolderItemVerbs verbs = link.Verbs();
for (int i = 0; i < verbs.Count; i++)
{
FolderItemVerb verb = verbs.Item(i);
string verbName = verb.Name.Replace(@"&", string.Empty).ToLower();
if ((verbName.Equals(localizedVerb.ToLower())))
{
verb.DoIt();
return true;
}
}
return false;
}
I’m also having issues with the “Pin to Start” functionality. I’ve downloaded the source code from part 2, changed the verb numbers in the action index and disabled the calling of the “ChangeImagePathName” routine and it confirms with an “OK” in the console but doesn’t actually do anything.
Two other tools I have ares still working using the verb number 51201 but this one is not. I’d really like to get this working as this is the most light-weight tool I’ve found to do this so far. Any help???
The option for “Prefer 32-bit” is greyed out (but un-ticked). I’m on Windows 10 Pro x64 VS2015 Community.
As an update… I found that it actually pins some shortcuts and exe’s to the Start Menu but not all where the other command line tools work on all .exe’s and .lnk’s. I need to try and figure out what the difference between the files are…
One example of file I can’t pin to the Start Menu is cmtrace.exe from the SCCM toolkit. It pins fine to the Task Bar but not to the Start Menu despite it reporting success.
Please see my Visual Studio project and CMTrace executable here…
https://www.dropbox.com/s/b3enefobxhfd9md/PintoTB10_test.zip?dl=1
Sorry to reply to myself twice (this always happens) but I think I’ve found what it is that stops icons pinning to the Start Menu in some circumstances…
It looks like it will only pin shortcuts to the Start Menu that already exist somewhere in the root of or in a subfolder of either…
%APPDATA%\Microsoft\Windows\Start Menu\Programs
or
%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu
So the solution is to perform a check for the filename to be pinned in either of those two paths and if not found copy it first (if it’s a .lnk shortcut) or create a shortcut if it’s .exe to %APPDATA%\Microsoft\Windows\Start Menu\Programs
I’m completely new to C# but I’ll have a go at this later if I can find some time…
Thanks StuartP79, you are right.. We can add up the code as you mentioned. That will solve the problem.
StuartP97, What I observed here is once we delete that shortcut from startmenu, pin also gets deleted automatically. So need to find any other alternative for that.
I added some code to make the shortcut and then pinning the new shortcut but I couldn’t make it pin without running the code again and then it pinned the new shortcut first time.
I think there is something missing in this method of pinning to the Start Menu as if you try to pin an exe manually that has no shortcut already in the start menu one is actually created in the location…
%APPDATA%\Microsoft\Windows\Start Menu\Programs
And yes, you are right, if you then delete the shortcut the pin disappears from the Start Menu but this is normal behaviour.
If someone can figure out how to bypass this issue for Start Menu pinning I’d be extremely grateful. For now I’ve added some code to the provided source to modify it for my purposes (general Taskbar and Start pinning for Windows 7 & 10 but with the current Start Menu pinning limitations)
https://www.dropbox.com/s/7rxpmff1zmamwpj/PinTo10v2_1.0.zip?dl=1
Hello Stuart & Alex,
I tried creating shortcut in startmenu and then pinning the application, it worked fine..I am sharing the code below which creates shortcut in startmenu. I have made a call to this function before calling
success = Utils.PinUnpinstartmenu(fileName, pin);
here PinUnpinstartmenu is a function which pins to start screen.
public static void CreateShortcut(string targetFileLocation)
{
Console.WriteLine(Environment.SpecialFolder.StartMenu.ToString());
string shortcutLocation = “C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Dummy.lnk”; //System.IO.Path.Combine(Environment.SpecialFolder.StartMenu.ToString(), “Dummy.lnk”);
WshShell shell = new WshShell();
Console.WriteLine(shortcutLocation);
IWshShortcut shortcut = (IWshShortcut)shell.CreateShortcut(shortcutLocation);
shortcut.Description = “Windows 10 Pinning dummy shortcut”; // The description of the shortcut
shortcut.TargetPath = targetFileLocation; // The path of the file that will launch when the shortcut is run
shortcut.Save(); // Save the shortcut
}
But my requirement is to pin an application to startscreen which does not have shortcut in startmenu.
I would be grateful if anyone can help me out with this problem.
Manish, I don’t think that’s possible. When you right-click a .exe that has no start menu shortcut and choose “Pin to Start” it creates a shortcut in the current user’s Start Menu folder. I’m pretty sure that this is part of the specifications of the process and can’t be altered.
Yes Stuart, you got it perfectly right.. Thanks for help. Now pinning works for me in one go. Do let me know your inputs about the code which I shared. Just incase its not working will share the complete code.. Thanks again…!!
I downloaded your first project, tried pinning cmtrace.exe and got the same results as you. I downloaded your second project and tried to pin cmtrace.exe, but I didn’t find the part where you create the shortcut. You create it manually?
I’ll try to update the project so it creates the shortcut automatically.
In my second project I completely scrapped the creatio of the shortcut as I couldn’t get it working. Looks like Manish got it working. I would like to see Manish’s full source code to see where I went wrong…
I am still making some changes in my code. Will share once completed.
I’ve added your code to my project and I can see that it’s creating a new shortcut but I’m still running into the same problem I ran into a few days ago that even when I create the shortcut programmatically before calling “Utils.PinUnpinStart(fileName, pin);” it won’t pin the shortcut the first time of running (but report “OK”) but will happily pin it on the following run but report that it “Failed”.
Manish, if you’ve overcome this I’d be very interested in seeing how…
Hello Stuart,
Even I started facing the same issue which you were talking about and solution I could find for that is after shortcut save add the below line
System.Threading.Thread.Sleep(5000);
Which would give 5secs of time for shortcut to get created properly and then pinning will take place.
Can you please test this and share the results.
I can confirm that adding a 5000ms sleep does seem to work but I really don’t want that to be the answer particularly as the PCs I hope to be using this on range from Core 2 Quads with 5 year old HDD’s up to i7’s with SSD’s and the shortcut creation process during user login could take quite some time on older hardware.
I actually did some trial and error testing and found that it would pin to the start menu after waiting 3300ms but not after 3200ms on my PC! I presume that’s probably linked to the time my CPU and SSD take to process the shortcut so couldn’t be used as a catch-all.
Is there any way to monitor the thread that starts when you run “shortcut.Save()” to verify when it is truly complete?
I am not sure on that but the idea what I have is we can run a loop to check if shortcut exists and will make to run untill shortcut exists. Once shortcut gets created the loop should end and proceed with remaining part of the code.Something like given below,
while(!File.Exists(filename))
{
System.Threading.Thread.Sleep(100);
}
Hope this works as expected.
Nope. I re-tested this as I’d already tried it and it doesn’t work. The file is created instantly after running shortcut.Save()
I added this…
shortcut.Save();
while (!System.IO.File.Exists(shortcutLocation))
{
Console.WriteLine(“Waiting 100ms…”);
System.Threading.Thread.Sleep(100);
}
if (System.IO.File.Exists(shortcutLocation))
{
Console.WriteLine(“Finished creating Start Menu icon”);
}
And it never triggered a wait at all. I am wondering if this is the right road to be going down as when triggering the verb manually in the context menu this is all triggered automatically…
oh okay.. Lets keep trying at our ends and will get in contact once we succeed. It would be better if we can contact though mail id or Social network (Facebook) as I am from India it would be difficult to cope up due to difference in timezones. What do you suggest Stuart?
I think I’ve done it (or at least worked around it)!
I’ve created a loop that keeps trying to pin to the Start Menu every 500ms and also looks for the unpin verb in the same loop so that it knows when it’s completed rather than take the success code from the verb.DoIt() action. If it still hasn’t succeeded after 20 attempts (10s) then it returns an error.
I need to add a little bit of refinement to some of the code and add some more checks for shortcut location / creation in the Start Menu but otherwise it’s functional. I’m sure good coders would tell me that I’ve duplicated too much code but never mind for now…
Yeah that was the second work around I was thinking about. I think what you have tried is the best possible way we could achieve as of now. Good stuff Stuart!
Hi Alex,
Good stuff, I have tried startmenu option and it works fine for 64bit applications but when I tried for 32bit applications it says done but actually it fails to pin. Although unpin works for both 32bit applications and 64bit applications.
Can you please help me out though this?
Thanks in advance.
Hello Stuart & Alex,
I tried creating shortcut in startmenu and then pinning the application, it worked fine..I am sharing the code below which creates shortcut in startmenu. I have made a call to this function before calling
success = Utils.PinUnpinstartmenu(fileName, pin);
here PinUnpinstartmenu is a function which pins to start screen.
public static void CreateShortcut(string targetFileLocation)
{
Console.WriteLine(Environment.SpecialFolder.StartMenu.ToString());
string shortcutLocation = “C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Dummy.lnk”; //System.IO.Path.Combine(Environment.SpecialFolder.StartMenu.ToString(), “Dummy.lnk”);
WshShell shell = new WshShell();
Console.WriteLine(shortcutLocation);
IWshShortcut shortcut = (IWshShortcut)shell.CreateShortcut(shortcutLocation);
shortcut.Description = “Windows 10 Pinning dummy shortcut”; // The description of the shortcut
shortcut.TargetPath = targetFileLocation; // The path of the file that will launch when the shortcut is run
shortcut.Save(); // Save the shortcut
}
But my requirement is to pin an application to startscreen which does not have shortcut in startmenu.
I would be grateful if anyone can help me out with this problem.
OK. I appreciate that I’ve removed the network app pinning functionality that this blog is all about but I’ve made quite a few changes to the initial code to make it work the way I wanted to and now have it pinning files to the Start Menu and Task Bar of Windows 7 & 10. I have overcome the Start Menu pinning bug that makes it not work if there is no existing shortcut anywhere in the Start Menu folder structure.
Here is my source and binary (.net 4.0 minimum required)
https://www.dropbox.com/s/q4joxy231hz0klj/PinTo10v2_1.1.zip?dl=1
Stuart
For someone who is not a programmer, how do run this from a batch file. I want to add office 2016 shortcuts to the taskbar.
I would like run it from my network \\e\Appdeploy\adminTools\Windows10\DeployShortCuts
Take a look at my modified / compiled version here (with a bit of info that should help)…
https://pinto10blog.wordpress.com/2016/09/10/pinto10/
Hi Hugh,
In the second part I have provided the compiled program, which you should be able to call from a batch file.
Good work Alex. Has anyone converted this to PowerShell yet?
Thanks
This seems to work. This is just the above code with some minor modifications.
Just run:
PinToTaskbar.ps1 ‘pathOrAppName’ ‘pin’
or
PinToTaskbar.ps1 ‘pathOrAppName’ ‘unpin’
#####################################################
param([String]$pathOrName=”0″, [String]$command=”0″)
$Assem = (“System.Linq”, “Microsoft.CSharp”)
$Source = @”
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Linq;
namespace Util
{
public static class Extension
{
public static IntPtr Increment(this IntPtr ptr, int value)
{
unchecked
{
if (IntPtr.Size == sizeof(Int32))
return new IntPtr(ptr.ToInt32() + value);
else
return new IntPtr(ptr.ToInt64() + value);
}
}
}
public class PinToTask
{
[StructLayout(LayoutKind.Sequential)]
public struct SHELLEXECUTEINFO
{
public int cbSize;
public uint fMask;
public IntPtr hwnd;
[MarshalAs(UnmanagedType.LPTStr)]
public string lpVerb;
[MarshalAs(UnmanagedType.LPTStr)]
public string lpFile;
[MarshalAs(UnmanagedType.LPTStr)]
public string lpParameters;
[MarshalAs(UnmanagedType.LPTStr)]
public string lpDirectory;
public int nShow;
public IntPtr hInstApp;
public IntPtr lpIDList;
[MarshalAs(UnmanagedType.LPTStr)]
public string lpClass;
public IntPtr hkeyClass;
public uint dwHotKey;
public IntPtr hIcon;
public IntPtr hProcess;
}
public enum ShowCommands : int
{
SW_HIDE = 0,
SW_SHOWNORMAL = 1,
SW_NORMAL = 1,
SW_SHOWMINIMIZED = 2,
SW_SHOWMAXIMIZED = 3,
SW_MAXIMIZE = 3,
SW_SHOWNOACTIVATE = 4,
SW_SHOW = 5,
SW_MINIMIZE = 6,
SW_SHOWMINNOACTIVE = 7,
SW_SHOWNA = 8,
SW_RESTORE = 9,
SW_SHOWDEFAULT = 10,
SW_FORCEMINIMIZE = 11,
SW_MAX = 11
}
[Flags]
public enum ShellExecuteMaskFlags : uint
{
SEE_MASK_DEFAULT = 0x00000000,
SEE_MASK_CLASSNAME = 0x00000001,
SEE_MASK_CLASSKEY = 0x00000003,
SEE_MASK_IDLIST = 0x00000004,
SEE_MASK_INVOKEIDLIST = 0x0000000c, // Note SEE_MASK_INVOKEIDLIST(0xC) implies SEE_MASK_IDLIST(0x04)
SEE_MASK_HOTKEY = 0x00000020,
SEE_MASK_NOCLOSEPROCESS = 0x00000040,
SEE_MASK_CONNECTNETDRV = 0x00000080,
SEE_MASK_NOASYNC = 0x00000100,
SEE_MASK_FLAG_DDEWAIT = SEE_MASK_NOASYNC,
SEE_MASK_DOENVSUBST = 0x00000200,
SEE_MASK_FLAG_NO_UI = 0x00000400,
SEE_MASK_UNICODE = 0x00004000,
SEE_MASK_NO_CONSOLE = 0x00008000,
SEE_MASK_ASYNCOK = 0x00100000,
SEE_MASK_HMONITOR = 0x00200000,
SEE_MASK_NOZONECHECKS = 0x00800000,
SEE_MASK_NOQUERYCLASSSTORE = 0x01000000,
SEE_MASK_WAITFORINPUTIDLE = 0x02000000,
SEE_MASK_FLAG_LOG_USAGE = 0x04000000,
}
private const int SW_SHOW = 5;
private const uint SEE_MASK_INVOKEIDLIST = 12;
[DllImport(“shell32.dll”, CharSet = CharSet.Auto)]
static extern bool ShellExecuteEx(ref SHELLEXECUTEINFO lpExecInfo);
public static void Go(string[] args)
{
if (args.Length < 2) return;
String filepath = args[0];
String command = args[1];
String dir = Path.GetDirectoryName(filepath);
String file = Path.GetFileName(filepath);
Boolean isModern = true;
if(filepath.StartsWith(@"\\") || filepath.Substring(1).StartsWith(@":"))
{
isModern = false;
}
switch (command.Trim().ToUpper())
{
case "PIN":
try
{
ChangeImagePathName("explorer.exe");
PinUnpinTaskbar(filepath, true, isModern);
}
finally
{
RestoreImagePathName();
}
break;
case "UNPIN":
try
{
ChangeImagePathName("explorer.exe");
PinUnpinTaskbar(filepath, false, isModern);
}
finally
{
RestoreImagePathName();
}
break;
}
}
public static bool PinUnpinTaskbar(string filePath, bool pin, bool isModern = false)
{
if (!File.Exists(filePath) && !isModern) throw new FileNotFoundException(filePath);
int MAX_PATH = 255;
var actionIndex = pin ? 5386 : 5387; // 5386 is the DLL index for"Pin to Tas&kbar", ref. http://www.win7dll.info/shell32_dll.html
//uncomment the following line to pin to start instead
//actionIndex = pin ? 51201 : 51394;
StringBuilder szPinToStartLocalized = new StringBuilder(MAX_PATH);
IntPtr hShell32 = LoadLibrary("Shell32.dll");
LoadString(hShell32, (uint)actionIndex, szPinToStartLocalized, MAX_PATH);
string localizedVerb = szPinToStartLocalized.ToString();
string path = isModern ? "shell:AppsFolder" : Path.GetDirectoryName(filePath);
string fileName = isModern ? filePath.Trim().ToUpper() : Path.GetFileName(filePath);
// create the shell application object
dynamic shellApplication = Activator.CreateInstance(Type.GetTypeFromProgID("Shell.Application"));
dynamic directory = shellApplication.NameSpace(path);
dynamic link = null;
if(!isModern)
link = directory.ParseName(fileName);
else
{
//// modern apps ////
var items = directory.Items;
foreach (var item in items)
{
//Console.WriteLine(item.Name);
if (item.Name.ToUpper() == fileName.ToUpper())
{
link = item;
break;
}
}
//// modern apps ////
}
dynamic verbs = link.Verbs();
for (int i = 0; i originalImagePathName.Length) throw new Exception(“new ImagePathName cannot be longer than the original one”);
//Write the string, char by char
var ptr = imageBuffer;
foreach (var unicodeChar in newImagePathName)
{
Marshal.WriteInt16(ptr, unicodeChar);
ptr = ptr.Increment(2);
}
Marshal.WriteInt16(ptr, 0);
//Write the new length
Marshal.WriteInt16(imageOffset, (short)(newImagePathName.Length * 2));
}
internal static void RestoreImagePathName()
{
IntPtr imageOffset, ptr;
GetPointers(out imageOffset, out ptr);
foreach (var unicodeChar in originalImagePathName)
{
Marshal.WriteInt16(ptr, unicodeChar);
ptr = ptr.Increment(2);
}
Marshal.WriteInt16(ptr, 0);
Marshal.WriteInt16(imageOffset, (short)(originalImagePathName.Length * 2));
}
public static ProcessBasicInformation GetBasicInformation()
{
uint status;
ProcessBasicInformation pbi;
int retLen;
var handle = System.Diagnostics.Process.GetCurrentProcess().Handle;
if ((status = NtQueryInformationProcess(handle, 0,
out pbi, Marshal.SizeOf(typeof(ProcessBasicInformation)), out retLen)) >= 0xc0000000)
throw new Exception(“Windows exception. status=” + status);
return pbi;
}
[DllImport(“ntdll.dll”)]
public static extern uint NtQueryInformationProcess(
[In] IntPtr ProcessHandle,
[In] int ProcessInformationClass,
[Out] out ProcessBasicInformation ProcessInformation,
[In] int ProcessInformationLength,
[Out] [Optional] out int ReturnLength
);
[StructLayout(LayoutKind.Sequential)]
public struct ProcessBasicInformation
{
public uint ExitStatus;
public IntPtr PebBaseAddress;
public IntPtr AffinityMask;
public int BasePriority;
public IntPtr UniqueProcessId;
public IntPtr InheritedFromUniqueProcessId;
}
private static string AssocQueryString(AssocStr association, string extension)
{
uint length = 0;
uint ret = AssocQueryString(
AssocF.ASSOCF_NONE, association, extension, “printto”, null, ref length);
if (ret != 1) //expected S_FALSE
{
throw new Win32Exception();
}
var sb = new StringBuilder((int)length);
ret = AssocQueryString(
AssocF.ASSOCF_NONE, association, extension, null, sb, ref length);
if (ret != 0) //expected S_OK
{
throw new Win32Exception();
}
return sb.ToString();
}
[DllImport(“Shlwapi.dll”, CharSet = CharSet.Unicode, SetLastError = true)]
private static extern uint AssocQueryString(
AssocF flags,
AssocStr str,
string pszAssoc,
string pszExtra,
[Out] StringBuilder pszOut,
ref uint pcchOut);
[DllImport(“kernel32”, SetLastError = true, CharSet = CharSet.Ansi)]
static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)]string lpFileName);
[DllImport(“kernel32.dll”, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool FreeLibrary(IntPtr hModule);
[DllImport(“user32.dll”, CharSet = CharSet.Auto)]
static extern int LoadString(IntPtr hInstance, uint uID, StringBuilder lpBuffer, int nBufferMax);
[Flags]
private enum AssocF : uint
{
ASSOCF_NONE = 0x00000000,
ASSOCF_INIT_NOREMAPCLSID = 0x00000001,
ASSOCF_INIT_BYEXENAME = 0x00000002,
ASSOCF_OPEN_BYEXENAME = 0x00000002,
ASSOCF_INIT_DEFAULTTOSTAR = 0x00000004,
ASSOCF_INIT_DEFAULTTOFOLDER = 0x00000008,
ASSOCF_NOUSERSETTINGS = 0x00000010,
ASSOCF_NOTRUNCATE = 0x00000020,
ASSOCF_VERIFY = 0x00000040,
ASSOCF_REMAPRUNDLL = 0x00000080,
ASSOCF_NOFIXUPS = 0x00000100,
ASSOCF_IGNOREBASECLASS = 0x00000200,
ASSOCF_INIT_IGNOREUNKNOWN = 0x00000400,
ASSOCF_INIT_FIXED_PROGID = 0x00000800,
ASSOCF_IS_PROTOCOL = 0x00001000,
ASSOCF_INIT_FOR_FILE = 0x00002000
}
private enum AssocStr
{
ASSOCSTR_COMMAND = 1,
ASSOCSTR_EXECUTABLE,
ASSOCSTR_FRIENDLYDOCNAME,
ASSOCSTR_FRIENDLYAPPNAME,
ASSOCSTR_NOOPEN,
ASSOCSTR_SHELLNEWVALUE,
ASSOCSTR_DDECOMMAND,
ASSOCSTR_DDEIFEXEC,
ASSOCSTR_DDEAPPLICATION,
ASSOCSTR_DDETOPIC,
ASSOCSTR_INFOTIP,
ASSOCSTR_QUICKTIP,
ASSOCSTR_TILEINFO,
ASSOCSTR_CONTENTTYPE,
ASSOCSTR_DEFAULTICON,
ASSOCSTR_SHELLEXTENSION,
ASSOCSTR_DROPTARGET,
ASSOCSTR_DELEGATEEXECUTE,
ASSOCSTR_SUPPORTED_URI_PROTOCOLS,
ASSOCSTR_PROGID,
ASSOCSTR_APPID,
ASSOCSTR_APPPUBLISHER,
ASSOCSTR_APPICONREFERENCE,
ASSOCSTR_MAX
}
}
}
“@
if (-not ([System.Management.Automation.PSTypeName]’Util.PinToTask’).Type)
{
Add-Type -ReferencedAssemblies $Assem -TypeDefinition $Source -Verbose
}
[Util.PinToTask]::Go(@($pathOrName, $command));
Code is badly formatted and prints errors. Can you please use pastebin.com ?
Is it possible to also pin a shortcut (.lnk with command line arguments) this way? I could not get it to work so far (guessing the renaming of the process works differently in this case)
sorry, I just tried it again and it seems to work, I must have mixed up something before
Hi!
I’ve downloaded the source, compiled it, and it’s working great. However, it doesn’t seem to want to exit. It just hangs with “OK” and a cursor on the next line.
I’ve tried converting the whole thing to void instead of int and throwing Environment.Exit(0) at the end instead, no dice. I’ve tried Compiling as 32 and 64 bit, no change.
I can probably use it in my solution as-is by running this and killing it 5-10sec later assuming success, but something more graceful is always preferred. Any thoughts?
Thanks; great applet!
~M
Hi Alex,
Any idea where I can find the action index’s for Windows Server 2016?
The code has check for “Windows 7” and “Windows 10”. You can edit source code and add WS2016.
Hello Alex,
This mechanism works smoothly on Windows 10 1803 and earlier Windows 10 versions.
However, it fails to work on Windows 10 1809.
Any suggestions would be of great help.
Yes, it appears Micro$haft figured this out and plugged the hole in 1809 or later. I completely understand they don’t want every installer in the universe clogging up the taskbar with garbage, but they fail to understand if you are living in a vertical market with non-savvy users, it’s a God-Send to put the little clicky thing right there for them to click on. *sigh*
i use this guys
http://www.technosys.net/products/utils/pintotaskbar