Search code examples
c#windowsiconsfile-attributesdesktop.ini

Apply Folder Icon Change


I am attempting to change the icon of a folder. The code below does all what I found online says to do but the icon never changes. Am I maybe not "Applying" the change?

string createdFile = Path.Combine(@"C:\Users\np\Desktop\PUTEST", "desktop.ini");
if (File.Exists(createdFile))
{
    var di = new DirectoryInfo(createdFile);
    di.Attributes &= ~FileAttributes.ReadOnly;

    File.Delete(createdFile);
    File.Create(createdFile).Dispose();
}
else
{
    File.Create(createdFile).Dispose();
}


//string iconPath = @"%SystemRoot%\system32\SHELL32.dll";
string iconPath = Environment.ExpandEnvironmentVariables(@"%SystemRoot%\system32\SHELL32.dll");
string iconIndex = "-183";
using (TextWriter tw = new StreamWriter(createdFile))
{
    tw.WriteLine("[.ShellClassInfo]");
    tw.WriteLine("IconResource=" + iconPath + "," + iconIndex);
    //tw.WriteLine("IconFile=" + iconPath);
    //tw.WriteLine("IconIndex=" + iconIndex);

}


File.SetAttributes(createdFile, System.IO.FileAttributes.ReadOnly);
File.SetAttributes(createdFile, System.IO.FileAttributes.System);
File.SetAttributes(createdFile, System.IO.FileAttributes.Hidden);

Solution

  • When crafting a file like this it's always good to do so using Explorer or Notepad first, then write/adjust your code to match whatever was produced. Otherwise, it's harder to figure out if the problem is with your file or your code.

    I believe the minimum requirements to make this work is Desktop.ini must be marked System and the parent directory must be marked ReadOnly (System may work there as well, but I know ReadOnly definitely does). So, your code is working with the right attributes, but there are still a few problems.

    Your if ... else ... block is saying "If a file exists at this path, create a directory at that path, then delete the file at that path, then create a file at that path." Of course, the directory should not and cannot have the same path as the file. I assume you are deleting and recreating the file to clear the contents when it already exists, however File.Create() overwrites (truncates) existing files, making the calls to both File.Delete() and File.Exists() unnecessary.

    More importantly is this line...

    di.Attributes &= ~FileAttributes.ReadOnly;
    

    ...with which there are two problems. First, you are ANDing the directory's attributes with the negation of ReadOnly, which has the effect of removing ReadOnly and keeping the other attributes the same. You want to ensure ReadOnly is set on the directory, so you want to do the opposite of the code you used: OR the directory's attributes with ReadOnly (not negated)...

    di.Attributes |= FileAttributes.ReadOnly;
    

    Also, you need that attribute set regardless of whether you created the directory or not, so that line should be moved outside of the if ... else ....

    Another issue is the successive calls to File.SetAttributes(). After those three calls the file's attributes will be only Hidden, since that was the value of the last call. Instead, you need to combine (bitwise OR) those attributes in a single call.

    A couple of other minor tweaks...

    • As you know since you are calling Dispose() on it, File.Create() returns a FileStream to that file. Instead of throwing it away, you could use it to create your StreamWriter, which will have to create one, anyways, under the covers. Better yet, call File.CreateText() instead and it will create the StreamWriter for you.
    • Environment variables are supported in Desktop.ini files, so you don't have to expand them yourself. This would make the file portable between systems if, say, you copied it from one system to another, or the directory is on a network share accessed by multiple systems with different %SystemRoot% values.

    Incorporating all of the above changes your code becomes...

    // Create a new directory, or get the existing one if it exists
    DirectoryInfo directory = Directory.CreateDirectory(@"C:\Users\np\Desktop\PUTEST");
    directory.Attributes |= FileAttributes.ReadOnly;
    
    string filePath = Path.Combine(directory.FullName, "desktop.ini");
    string iconPath = @"%SystemRoot%\system32\SHELL32.dll";
    string iconIndex = "-183";
    
    using (TextWriter tw = File.CreateText(filePath))
    {
        tw.WriteLine("[.ShellClassInfo]");
        tw.WriteLine("IconResource=" + iconPath + "," + iconIndex);
        //tw.WriteLine("IconFile=" + iconPath);
        //tw.WriteLine("IconIndex=" + iconIndex);
    }
    
    File.SetAttributes(filePath, FileAttributes.ReadOnly | FileAttributes.System | FileAttributes.Hidden);
    

    One catch is that the above code throws an exception if you run it twice in succession. This is because the File.Create*() methods fail if the input file is Hidden or ReadOnly. We could use new FileStream() as an alternative, but that still throws an exception if the file is ReadOnly. Instead, we'll just have to remove those attributes from any existing input file before opening it...

    // Create a new directory, or get the existing one if it exists
    DirectoryInfo directory = Directory.CreateDirectory(@"C:\Users\np\Desktop\PUTEST");
    directory.Attributes |= FileAttributes.ReadOnly;
    
    string filePath = Path.Combine(directory.FullName, "desktop.ini");
    FileInfo file = new FileInfo(filePath);
    
    try
    {
        // Remove the Hidden and ReadOnly attributes so file.Create*() will succeed
        file.Attributes = FileAttributes.Normal;
    }
    catch (FileNotFoundException)
    {
        // The file does not yet exist; no extra handling needed
    }
    
    string iconPath = @"%SystemRoot%\system32\SHELL32.dll";
    string iconIndex = "-183";
    
    using (TextWriter tw = file.CreateText())
    {
        tw.WriteLine("[.ShellClassInfo]");
        tw.WriteLine("IconResource=" + iconPath + "," + iconIndex);
        //tw.WriteLine("IconFile=" + iconPath);
        //tw.WriteLine("IconIndex=" + iconIndex);
    }
    
    file.Attributes = FileAttributes.ReadOnly | FileAttributes.System | FileAttributes.Hidden;
    

    I changed from using File to FileInfo since that makes this a little easier.