Jonathan's profileJon's Vista Gadgets BlogBlogLists Tools Help

Blog


    June 03

    Power Plan Gadget

    I've recently posted a "Work in Progress" Gadget on AeroXperience, that changes the current Vista Power plan based on currently running processes.  I wrote the Gadget because whilst playing Assassins Creed, I noticed Vista was lowering the CPU to 50% - which was dropping the frame rate by around 15FPS.

    I normally leave the power plan on Balanced to save power, but got fed up having to manually change it to Performance whenever I played AC.

    drag

    Download here.

    April 24

    HOW TO: Debugging Gadgets

    Debugging Gadgets can be a little frustrating, not only whilst coding; but also when trying to get useful feedback from users who've reported issues.

    The simplest way to achieve this, is to log debug entries out to a file.  In the example below, we check to see if "debug.txt" exists in the Gadget folder and if it does write debugging information to it.

    The first step is to see if the file exists and open it.  In the following code, we create a BOOLEAN variable "bDebug" which we can use later to output more detailed information, beyond simply errors:

    var bDebug = oFSO.FileExists(gadgetPath+"\\debug.txt");
    try{
    if (bDebug)
      var debugLogFile = oFSO.OpenTextFile(gadgetPath+"\\debug.txt", 2);
    } catch(err) {bDebug = false; debugLog("Open debug.txt error: "+err.name+" - "+err.message)}

    //log a debug entry
    function debugLog(str) {
        try{
            System.Debug.outputString(str);
            if (bDebug) debugLogFile.WriteLine(str);
        } catch(err) {}
    }

    It's good practice to get into the habit of wrapping all your functions in a try/catch loop, to prevent Runtime errors from popping up:

    try{
      // your code
    } catch (err) {debugLog(err.name+ " - "+err.message)}

    It's also a good idea to put in tracking information, so you know where you are in the code.  For example:

    if(bDebug) debugLog("Register ActiveX");
    if(bDebug) debugLog("  trying HKCU");
    if (!activateXDLL("HKCU")) {
        if(bDebug) debugLog("  trying HKLM");
        if (!activateDLL("HKLM"))
            debugLog("Error creating ActiveX object");
    }

    To enable "debug mode", simply create a blank text file in your Gadget folder called "debug.txt".

    Personally, I put this code into all Gadgets and leave "debug.txt" out of the Gadget package.  When a user reports an issue, I ask them to create the file and eMail it back to me once they've reproduced the problem.

     

    Whilst your developing your code, I'd also advise running Microsoft's Debug View; which will allow you to watch all debug entries as they occur.

    In a future article, I'll detail how to write out debugging information from VB and C++ ActiveX DLL's.

    HOW TO: Implementing Drag / Drop in Gadgets


    Allowing drag/drop

    First, you need to set up the HTML events to allow drag/drop. By default they're disabled, so you need to allow them. You do this by cancelling two drag events on the <BODY> tag:

    <BODY
    ondragenter="java script:event.returnValue = false"
    ondragover="java script:event.returnValue = false"
    >



    The next thing you need to do, is set up a function to handle the drag/drop action. This is also done on the <BODY> tag with the ondrop event. In this example, the function is fileDragDropped(), so your final <BODY> tag should be:

    <BODY
    ondragenter="java script:event.returnValue = false"
    ondragover="java script:event.returnValue = false"
    ondrop="fileDragDropped"
    >




    Handling File drag/drop from Explorer

    Files are passed through event.dataTransfer as an object with a collection of items inside. To extract each entry you need to use System.Shell.itemFromFileDrop(<object>, <n>). To avoid errors, you need to loop whilst the object is not null.

    function fileDragDropped() {
        var  sFile;

        for (var i=0; System.Shell.itemFromFileDrop(event.dataTransfer, i) != null; i++)
        {
            // get the file name and path
            sFile = System.Shell.itemFromFileDrop(event.dataTransfer, i).path;

            // do something with sFile
        }
    }




    Trapping for certain file types

    In most cases, you'll want to handle only certain file types. So you need to check the file extension and ignore files that are not supported. The best way to do this is through a string that contains all your supported extensions, you then check if the file extension is in that string. supportedFileTypes contains all your extensions padded with a space either side, to stop it catching truncated extensions (eg ".doc" but not ".document"). In this example, we're checking for .doc, .xls and .ppt files:

    var supportedFileTypes = " .doc .xls .ppt ";

    function fileDragDropped() {
        var sFile, sFiletype;

        for (var i=0; System.Shell.itemFromFileDrop(event.dataTransfer, i) != null; i++)
        {
            // get the file name and path
            sFile = System.Shell.itemFromFileDrop(event.dataTransfer, i).path;

            // get the extension
            sFiletype = " " + sFile.substring(sFile.lastIndexOf(".")).toLowerCase() + " ";

            if (supportedFileTypes.indexOf(sFiletype) > -1) {
                // do something with sFile
            }
        }
    }





    Handling URL drag/drop from Internet Explorer

    Internet Explorer passes information slightly differently, URL's are passed as text, so you need to change the code slightly to handle these. The URL you end up with is a string and to ensure we don't pick up files being drag/dropped, we need to check if that string is null:

    function fileDragDropped() {
        var url = event.dataTransfer.getData("text");
        if (url != null) {
            // do something with the url
        }
    }





    Putting it all together

    Combining both IE and file/drag drop together, your final function will be:

    var supportedFileTypes = " .doc .xls .ppt ";

    function fileDragDropped() {
        var sFile, sFiletype;

        // is it a URL from IE?
        var url = event.dataTransfer.getData("text");

        if (url != null) {
            // do something with the url
        } else {
            for (var i=0; System.Shell.itemFromFileDrop(event.dataTransfer, i) != null; i++)
            {
                // get the file name and path
                sFile = System.Shell.itemFromFileDrop(event.dataTransfer, i).path;

                // get the extension
                sFiletype = " " + sFile.substring(sFile.lastIndexOf(".")).toLowerCase() + " ";
       
                // is the extension supported
                if (supportedFileTypes.indexOf(sFiletype) > -1) {
                    //do something with sFile
                }
            }
        }
    }
    October 03

    HOW TO: Skin selection

    You want to provide your users with a visual skin selector in your settings, but don't know where to start.  Here's all the code you'll need to do it.  The end result will look something like this:

    skinSelection

     

    File structure

    Firstly, create a file structure to support skins.  You'll be placing all the skins in subfolders under a themes folder.

    <Gadget Folder>
        images
        themes
            <skin1 folder>
            <skin2 folder>

    This keeps all your skins out of the way, and easy to distinguish from the rest of the Gadget.  Each skin subfolder should be named as you'd like the skin name to appear to the user.  ie Default, Blue, Black etc.  And each folder needs to contain a folder.png file, which should be a screenshot of the Gadget with the skin applied.  This image should ideally be scaled and no larger than 100x100 pixels.

     

    Images

    You'll need some images for the previous/next buttons, so save the following images in the images folder created above.

    left.png: Left leftdown.png: LeftDown lefthover.png: LeftHover
    right.png: Right rightdown.png: RightDown righthover.png: RightHover

     

    Settings.html

    The settings file needs to search the themes subfolder, store the options and select the current skin.

    Without going into too much detail, it will create an array of the subfolders under themes and using the gSkin stored setting, select the currently selected skin.  The only entry you need to change is the default skin name, which is the first line of the script.

    <html>
     <style>
         * {
          font-family:Segoe UI;
          font-size:12px;
         }

      body{
       width:300px;
       height:250px;
       margin:0px;
      }
     </style>
     <head>
      <script language="javascript" type="text/javascript">
       var defaultSkin = "Default";   // default skin name
       var maxSkins = 256;  // max number of skins allowed

       System.Gadget.onSettingsClosing = settingsClosing;

       var oFSO = new ActiveXObject("Scripting.FileSystemObject");
       var aSkins = new Array(maxSkins);
       var skinCount = 0;        // number of skins found
       var currentSkinNo = 0;    // the current skin selected

       var currentSetting = System.Gadget.Settings.readString("gSkin");
       if (currentSetting == "")
        currentSetting = defaultSkin;

       
       function init() {
           getSkins();
           updateSkin();
       }
       
       function settingsClosing(event) {
           try{
                if (event.closeAction == event.Action.commit) {
                   //write settings to INI
                   System.Gadget.Settings.write("gSkin", aSkins[currentSkinNo].substring(aSkins[currentSkinNo].lastIndexOf("\\") + 1, aSkins[currentSkinNo].length));
       
                   // update the Gadget if the skin has changed
                   if (currentSetting != System.Gadget.Settings.readString("gSkin"))
                       updateGadget();
               }
           } catch(err) {System.Debug.outputString("settingsClosing: " + err.name+" - "+err.message)}
       
           event.cancel = false;
       }
       
       function updateSkin() {
           imgSkin.src = aSkins[currentSkinNo] + "\\folder.png";
           divCount.innerText = (currentSkinNo + 1) + "/" + skinCount;
           divName.innerText = aSkins[currentSkinNo].substring(aSkins[currentSkinNo].lastIndexOf("\\") + 1, aSkins[currentSkinNo].length);
       }
       
       // Read skin folders
       function getSkins() {
           var oFolder = oFSO.GetFolder(System.Gadget.path + "\\themes");
           var oSubFolders = new Enumerator(oFolder.SubFolders);
       
           try{
               for (; !oSubFolders.atEnd() && skinCount < maxSkins; oSubFolders.moveNext())
               {
                   aSkins[skinCount] = String(oSubFolders.item());
       
                   // have we found the current skin
                   if(aSkins[skinCount].toLowerCase() == (System.Gadget.path.toLowerCase() + "\\themes\\" + currentSetting).toLowerCase())
                       currentSkinNo = skinCount;
       
                   skinCount++;
               }
           } catch(err) {System.Debug.outputString("getSkins: " + err.name + " - " + err.message)}
       }
       
       // Display the next Skin
       function nextSkin() {
           currentSkinNo = (currentSkinNo+1) % skinCount;
           updateSkin();
       }
       
       // Display the previous Skin
       function previousSkin() {
           currentSkinNo = (currentSkinNo-1+skinCount) % skinCount;
           updateSkin();
       }
       
       // Change a IMG source
       function changeIMG(imagesource, imagename) {
           imagesource.src= "images\\" + imagename;
       }
      </script>
     </head>
     <body onload="init()">
      <div style="position:static; width:100%; height:160px;">
       <div style="position:absolute; top:0px; left:0px; width:100%; height:100px; text-align:center;">
        <div style="width:128px; height:100%; background-color:white; border-style: solid; border-width:1px; border-color:rgb(180,180,180); overflow:hidden;">
         <div style="position:absolute; top:50%; left:50%;">
          <img id="imgSkin" src="images/blank.png" style="position:relative; top:-50%; left:-50%"/>
         </div>
        </div>
       </div>
       <div style="position:absolute; width:100%; height:50px; top:110px; left:0px; text-align:center;">
        <div style="position:absolute; top:0px; left:50%;">
         <div style="position:absolute; left:-35px;">
            <img id="imgPreviousSkin" title="Previous" src="images/left.png"
           style="position:relative; left:-50%;"
           onMouseOver="javascript:changeIMG(imgPreviousSkin,'LeftHover.png')"
           onMouseOut="javascript:changeIMG(imgPreviousSkin,'Left.png')"
           onClick="previousSkin()"
          />
         </div>
           <div style="position:absolute; left:35px;">
              <img id="imgNextSkin" title="Next" src="images/right.png"
               style="position:relative; left:-50%;"
           onMouseOver="javascript:changeIMG(imgNextSkin,'RightHover.png')"
           onMouseOut="javascript:changeIMG(imgNextSkin,'Right.png')"
           onClick="nextSkin()"
          />
         </div>
        </div>
        <div style="position:absolute; top:2px; left:50%;">
         <div id="divCount" style="position:relative; left:-50%;"></DIV>
        </div>
        <div style="position:absolute; top:22px; left:50%;">
         <B><DIV id="divName" style="position:relative; left:-50%;"></DIV></B>
        </div>
       </div>
      </div>
       </body>
    </html>

     

    The only function not coded above is updateGadget(), which needs to apply the skin to the Gadget.  An example would be to reload the Gadget:

    function updateGadget() {
        try{
            System.Gadget.document.parentWindow.location.href = System.Gadget.document.parentWindow.location.href;
        } catch(err) {}
    }

    Or, call the parent function that loads the skin (see Loading the Gadget skin):

    function updateGadget() {
        try{
            System.Gadget.document.parentWindow.loadSkin();
        } catch(err) {}
    }

     

    Loading the Gadget skin

    This is the tricky bit and depends very much on how complicated you want your skins to be.  It can also get very complicated if you're supporting docked/undocked states and left/right handed sidebars.  For sanity, I've assumed we have only one Gadget state in the following examples.

    Option 1 -  The skin contains graphics only, in which case you just need to update the URL's of your images. eg.

    var defaultSkin = "Default";
    function loadSkin() {
        // use the default skin if there's no saved setting
        if (System.Gadget.Settings.readString("gSkin") != "")
            var skinPath = "themes/" + System.Gadget.Settings.readString("gSkin") + "/";
        else
            var skinPath = "themes/" + defaultSkin + "/";

        // update the Gadget background and images
        System.Gadget.background = "url(" + skinPath + "background.png)";
        img1.src = skinPath + "img1.png";
        img2.src = skinPath + "img2.png";
        ...
    }

     

    Option 2 - As well as containing images, the skin also has a stylesheet called skin.css. To see this in action, look at Media Player

    var defaultSkin = "Default";
    function loadSkin() {
        // use the default skin if there's no saved setting
        if (System.Gadget.Settings.readString("gSkin") != "")
            var skinPath = "themes/" + System.Gadget.Settings.readString("gSkin") + "/";
        else
            var skinPath = "themes/" + defaultSkin + "/";

        // create the stylesheet if it doesn't exist
        var x = document.getElementsByTagName('link');
        if (x.length > 0)
            x[0].href = skinPath + "skin.css";
        else {
            var oLink = document.createElement("link");
            oLink.href = skinPath + "skin.css";
            oLink.rel = "stylesheet";
            oLink.type = "text/css";
            document.body.appendChild(oLink);
        }
        // update the Gadget background and images
        System.Gadget.background = "url(" + skinPath + "background.png)";
        img1.src = skinPath + "img1.png";
        img2.src = skinPath + "img2.png";
        ...
    }

    Note that when applying a stylesheet, due to bugs in Sidebar, several style settings aren't applied.  Body background images, body width and body height to name but a few.  If you skins change the Gadget size, you'll have to code around this by reading and applying the stylesheet settings yourself.

     

    Option 3 - The skin contains code.  To see this in action, look at Spectrum Analyser.

    This is ironically the easiest option, as you can leave the skin to initialise the Gadget's graphics etc.  Simply append the following code immediately before the closing </html> statement in your gadget.html file:

    <script language="javascript" type="text/javascript">
        try{
            if (System.Gadget.Settings.readString("gSkin") != "")
                var skinPath = "themes/" + System.Gadget.Settings.readString("gSkin") + "/";
            else
                var skinPath = "themes/" + defaultSkin + "/";

            var scriptInclude = "<script src=\"" + skinPath + "skin.js\" language=\"javascript\" type=\"text/javascript\"></script" + ">";
            document.body.appendChild(document.createElement(scriptInclude));
        } catch(err) {System.Debug.outputString("loadSkin: " + err.name + " - " + err.message)}
    </script>

    Your skins should then use the skinPath variable for all local file references.

    September 27

    Spectrum Analyser updated

    There's a new version of Spectrum Analyser available on AeroXP.

    drag

    September 20

    WMP Gadget/Streamed media issues.

    For the past month, I've been investigating an issue where embedding WMP into a Gadget causes Sidebar to not trust Gadget code.  Obvious symptoms are "Access Denied" errors when Gadgets try to access local files or Flyout/Gadget/Settings variables.  The problem centers around security updates released on the 14th August, notably KB937143.

    I've not actually found what the cause is, but removing the update didn't resolve the issue.  However reinstalling it seems to resolve it.  I can only guess that there's either a conflict with other updates or the install is failing in some way, which isn't visible.

    I expect this isn't the end of the story, as I'm not convinced this is either the cause or the fix.  I'm fairly certain that something else is coming into play here, that I've missed.  Unfortunately, the problem isn't always easily reproducible, it's a random issue that occurs after no obvious event.

    An early workaround I found was to put an invalid "Mark of the Web" in the HTML header.  Although this stopped the issue occurring virtually immediately (playing a stream would immediately brand the Gadget untrusted), the problem would reoccur if a WMP Gadget was left open for any length of time.

    September 12

    Network Utilization

     

    If you use Network Utilization, you might want to have a look at Network Traffic, which is based on my Gadget, but with a nifty front end:

    folder

    And whilst you're on the site, have a look at Volume Control Reloaded, which uses the new Vista Sound API:

    vcr_screen

    August 30

    Work in Progress

    I have several Gadgets in progress on AeroXP, if you want to try/make suggestions.  You can download them from here.

    Spectrum Analyser

    Spectrum Analyser

    If Microsoft didn't make it so difficult, this Gadget would probably be quite interesting.  It shows a realtime EQ of your soundcard's output.  Unfortunately, you have to do some messing around to get around Vista's DRM, to get it working.  It does implement a rather nifty skin model, that allows you to add your own code.


    Mandelbrot Explorer

    Mandelbrot Explorer

    This Gadget just flies in and out of the Mandelbrot set, completely useless to all - I coded it because whilst coding Spectrum Analyser I stumbled upon a Mandelbrot generator I coded in ARM Assembler 20 years ago.
    If you look near the bottom of the thread, I've also posted a Mandelbrot generator EXE that I coded, that does the whole thing on the GPU, in a Pixel Shader.  You'll need an XB360 and a graphics card that supports Pixel Shader 3 to be able to use it though.


    Flip Calendar

    Flip Calendar

    This is a simple calendar Gadget someone asked me to write for them.  Very basic, but someone might find it useful.

    Testers required

    If you have a Wireless card, I need some testers to confirm that the latest version of Network Utilization does handle the WiFi NIC appearing/disappearing.

    You can grab the latest version on AeroXP, here.  I think you need to setup a free account before you can get to it, which you can do from the home page.

    Sidebar known bugs list growing

    My Known Bugs in Sidebar list is now up to 52 known issues and bugs, if you've not looked at it recently.

    Windows update causes problems for some Gadgets

    I noticed the other day the Media Player was doing strange things when playing streamed media.  After a few hours of testing over the past two weeks, another developer notice something similar happening in their media gadget, which made me immediately rule out my code and start looking at Windows Updates.

    In short, KB937143 causes Gadgets that embed WMP to become untrusted.  Symptoms include Flyouts, Settings and the main Gadget HTML to all refuse to see to each other.

    By embedding a Mark of the Web, you can over come this problem - for a few minutes (without it, playing a stream causes the Gadget to become untrusted immediately).  I'm not sure at the moment if it's a timing issue, or an event that causes the Gadget to become untrusted again after ~10 mins.  I'll continue testing.
    July 03

    How to find the IFrame size in WPF

    Whilst coding a Mandelbrot explorer, I needed to find the IFrame size in pixels to produce a Bitmap the same size.  After a lot of wasted time searching the net, via trial and error I came up with the following method:

    Assuming you're using a Grid to hold your elements:

    <Grid Name="myGrid">
       ...
    </Grid>

    To get the IFrame's size, we get the XAML page's parent, which is a ContentPresenter that holds the width/heigh in pixels:

    Dim browser As System.Windows.Controls.ContentPresenter = VisualTreeHelper.GetParent(myGrid.Parent)
    Dim width As Integer = browser.ActualWidth
    Dim height As Integer = browser.ActualHeight
    June 13

    More bugs and issues

    I've got a few bugs/issues to document, so keep an eye on Known bugs in Sidebar RTM over on AeroXperience.  The first four were discovered last year, the rest are recent, some from fellow developers:

    I've not spent any time checking these, or trying to create repros, so workarounds are unknown at the moment.

    1. <g:text> quality.  g:text can produce very bad aliasing
    2. <g:image/g:background>.softEdge doesn't work on an image at 45 degrees
    3. Mouse events don't work on pixels where the opacity is <100%
    4. Magenta in GIF's becomes transparent
    5. [James Ross] System.Shell.Item.isFile is unusable
    6. [David E. Craig] Changing the Settings page via location.replace(<filename>) causes the page to go blank
    7. Desktop DPI affects Gadget text sizes (as mentioned in a previous article)
    8. Overlaying <g:background> where opacity has been set <100%, with an image containing transparent pixels, causes the whole image to gain the <g:background> opacity when the Gadget loses focus (variation on bug#32)
    June 07

    DPI and Fonts in Gadgets

    An issue which I've overlooked in Gadgets until recently. What happens to the them when the Font DPI is changed from the default setting?

    If the user changes the DPI from 96 to 120, all fonts become larger and consequently Gadget that have text often look a mess.

    The workaround is actually quite simple. Firstly you need to specify the default font size, to ensure it's the same no matter what the DPI setting is. You can do this by adding an entry to your main Gadget, Settings and Flyout HTML files. This needs to be placed at the top of your STYLE section or CSS file:

    * {font-size:12px;}

    Secondly, all explicit font-size entries must be specified in px as above. Unlike other font units px is not linked to DPI size.


    Another font issue in Gadgets is the inconsistent default font styles. INPUT and SELECT entries default to "Arial", body text, DIV's etc default to "Tahoma". If you want you Gadget to look consistent, specify the default font-family as well as the default size:

    * {font-size:12px; font-family:Segoe UI;}
    May 09

    How to determine which side the Flyout opened on

    If you need to determine which side your Flyout opened, relative to your Gadget, you need to compare window.screenLeft of the Gadget parent to the Flyout.  Nothing of course is that simple, as the Flyout screen position isn't determined until the Flyout has been composed.  The trick is to repeatedly check the Flyout's position until it has been defined.

    Add this code to the HEAD section:

        var offScreen = window.screenLeft;
       
        function checkFlyout() {
            if (window.screenLeft == offScreen)
            {
                setTimeout(checkFlyout, 100);
                return;
            }
       
            if (window.screenLeft > parseInt(System.Gadget.document.parentWindow.screenLeft)) {
               document.body.innerHTML = "right";
            } else {
               document.body.innerHTML = "left";
            }
        }

    and then add an onload event to the Flyout body:

    onload="checkFlyout()"

    March 20

    HOW TO: Implementing a Settings dialogue

    There's many ways to implement the Settings page, this being one.  Alternate methods include using parent Gadget variables to hold settings and coding the Settings page to refresh the main Gadget.  In this method, we're following Microsoft best practice, where the Settings and Gadget code are independent and update themselves if the other changes a setting.

    First the main Gadget code, where you need to initialise the settings and read and saved settings in.

    You need to read/initialise the Settings in the HEAD section. It's a good idea to write default settings back immediately if they aren't available, so they're available to Settings.html without any validation.

    Another important factor is to always read string values with readString, to ensure the variable type is correct.

    //initialize default settings variable values
    var mySetting1 = 10;
    var mySetting2 = 50;
    var mySetting2 = "Hello";

    //read the settings
    readSettings();

    function readSettings() {
      //check if they exist by checking if the first setting exists
      if (System.Gadget.Settings.readString("mySetting1") == "") {
        //no, save the defaults
        System.Gadget.Settings.write("mySetting1", mySetting1);
        System.Gadget.Settings.write("mySetting2", mySetting2);
        System.Gadget.Settings.write("mySetting3", mySetting3);
      } else {
        mySetting1 = System.Gadget.Settings.read("mySetting1");
        mySetting2 = System.Gadget.Settings.read("mySetting2");
        mySetting3 = System.Gadget.Settings.readString("mySetting3");
      }
    }

    To avoid type issues, we're converting mySetting1 to a string before checking if it exists. If we didn't do this, the setting would appear to not exist if it's value was zero.



    Settings dialogue:

    Initialising a settings dialogue

    To let Sidebar know you have a settings dialogue, you need to include the following line in the HEAD section of gadget.html

    System.Gadget.settingsUI = "settings.html";


    The Settings.html file itself

    Because we know the settings have been initialised, the code is very simple. Here's the complete Settings.html file.  The settingsClosing function writes the values back, if the user has clicked the OK buttom.

    <HTML>
    <HEAD>
    <SCRIPT language="javascript" type="text/javascript">
    System.Gadget.onSettingsClosing = settingsClosing;

    //Save the settings
    function settingsClosing(event) {
      if (event.closeAction == event.Action.commit) {
        //User has clicked OK so commit the settings
        System.Gadget.Settings.write("mySetting1", input1.value);
        System.Gadget.Settings.write("mySetting2", input2.value);
        System.Gadget.Settings.write("mySetting3", input3.value);
      }

      //allow the settings page to close
      event.cancel = false;
    }

    function init() {
      input1.value = System.Gadget.Settings.read("mySetting1");
      input2.value = System.Gadget.Settings.read("mySetting2");
      input3.value = System.Gadget.Settings.readString("mySetting3");
    }
    </SCRIPT>
    </HEAD>

    <BODY onload="init()" style="width:200px; height:100px; margin:0px">
      <input type="text" id="input1" title="Setting#1" size=3>
      <input type="text" id="input2" title="Setting#2" size=20>
    </BODY>
    </HTML>



    Reading the settings back, once the Settings dialogue has close

    We'll need to know if settings have been changed in the settings dialogue. To do this, we setup a function in gadget.html and tell Sidebar to call it once the settings page has closed. Again, place the code in the HEAD section of gadget.html:

    System.Gadget.onSettingsClosed = settingsClosed;
     
    function settingsClosed() {
      //read the settings back in
      readSettings();
     
      //code to update your gadget if necessary
    }
     
    Your final gadget.html should be:
     
    //initialize default settings variable values
    var mySetting1 = 10;
    var mySetting2 = 50;
    var mySetting2 = "Hello";
    System.Gadget.settingsUI = "settings.html";
    System.Gadget.onSettingsClosed = settingsClosed;

    //read the settings
    readSettings();

    function readSettings() {
      //check if they exist by checking if the first setting exists
      if (System.Gadget.Settings.readString("mySetting1") == "") {
        //no, save the defaults
        System.Gadget.Settings.write("mySetting1", mySetting1);
        System.Gadget.Settings.write("mySetting2", mySetting2);
        System.Gadget.Settings.write("mySetting3", mySetting3);
      } else {
        mySetting1 = System.Gadget.Settings.read("mySetting1");
        mySetting2 = System.Gadget.Settings.read("mySetting2");
        mySetting3 = System.Gadget.Settings.readString("mySetting3");
      }
    }
    function settingsClosed() {
      //read the settings back in
      readSettings();
     
      //code to update your gadget if necessary
    }

    March 13

    Sidebar bugs

    If you've tried doing anything beyond a simple HTML page in Sidebar, you're bound to have come across one of it's many bugs. Personally, I've found it very frustrating as every Gadget I've written, I've ended up spending more time finding workaround's for bugs then coding the Gadget. Polaroid is a good example of this, which exposed 9 bugs! I ended up recoding it four times before it was usable, finally ending up with a combination of VML and <g:background>/<g:image> to get the result you see now.

    Out of the Gadget's I've coded so far, two were bug free, the bug totals for the others were as follows:

    Asteroids (4, 1 being a bug in VML)
    Calvin and Hobbes (2)
    CPU Utilization (2)
    Network Utilization (2)
    Media Player (5, 1 being a bug in IE)
    Polaroid (9)


    Polaroid took around 30 mins to code, followed by around 30 hours figuring out workarounds for all the Sidebar bugs. I was determined to prove it could be done though, so stuck with it.
    I'm still not completely happy with the result; the image jitters like crazy when you rotate it, which seems to be an issue with Sidebar's transitioning code. Simple you say, wrap the code in begin/endTransition - but this results in an unusable Gadget which is extremely slow and sometimes corrupts the screen due to more "issues" in Sidebar - sometimes you just can't win.

    Asteroids was probably the most frustrating, as I had to content with an annoying bug in VML which ironically was the only way to fix a bug in Sidebar, so I ended up spending over a month trying different methods to work around both.
    The main bug was screen corruption when the VML objects were updated. For some reason, putting all the VML objects into a VML groups fixed this. It however highlighted a bug in VML which caused the objects to jitter by anything up to ~50 pixels each time an object's top/left was touched. So I recoded it to put every object into it's own VML group, which looked perfect - except it was around 400% slower. For another odd reason, using VML groups really slows down VML.

    In the end, I opted for a compromise where I put each ship/asteroid in it's own VML groups and put all text and dust into another group. The end result, it's a lot faster, but the dust jitters around the screen. Which is really noticable when your ship explodes.

    I started coding Gadgets to assist Microsoft's Sidebar team in the debugging process, so I've continued that process by documenting the bugs, producing Repro's and figuring out workarounds where possible.  You can see the full bug list along with examples and workarounds here

    Supporting Drag/Drop

    If you want your Gadgets to do something with URL's and Files when they're drag/dropped on it, here's how.


    Allowing drag/drop

    First, you need to setup the HTML side to allow for drag/drop. By default they're disabled, so you need to allow them. You do this by canceling two drag events on the <BODY> tag:
    <BODY
      ondragenter="javascript:event.returnValue = false"
      ondragover="javascript:event.returnValue = false"
    >
    The next thing you need to do, is setup the function to handle the drag/drop action. This is also done on the <BODY> tag with the ondrop event. In this case, the function is fileDragDropped(), so your final <BODY> tag should be:
    <BODY
      ondragenter="javascript:event.returnValue = false"
      ondragover="javascript:event.returnValue = false"
      ondrop="fileDragDropped"
    >
    Handling File drag/drop from Explorer

    Files are passed through event.dataTransfer, as an object with a collection of items inside. To extract each entry you need to use System.Shell.itemFromFileDrop(<object>, <n>). To avoid errors, you need to loop whilst the object is not null.


    function fileDragDropped() {
      var sFile;

      for (var i=0; System.Shell.itemFromFileDrop(event.dataTransfer, i) != null; i++)
      {
        // get the file name and path
        sFile = System.Shell.itemFromFileDrop(event.dataTransfer, i).path;

        // do something with sFile
      }
    }

    Trapping for certain file types

    In most cases, you'll want to handle only certain file types. So you need to check the file extension and ignore files that are not supported. The best way to do this is through a string that contains all your supported extensions, you then check if the file extension is in that string. supportedFileTypes contains all your extensions padded with a space either side, to stop it catching truncated extensions (ie we want to catch ".doc" but not ".document"). In this example, we're checking for .doc, .xls and .ppt files:
    var supportedFileTypes = " .doc .xls .ppt ";
     
    function fileDragDropped() {
      var sFile, sFiletype;

      for (var i=0; System.Shell.itemFromFileDrop(event.dataTransfer, i) != null; i++)
      {
        // get the file name and path
        sFile = System.Shell.itemFromFileDrop(event.dataTransfer, i).path;

        // get the extension
        sFiletype = " " + sFile.substring(sFile.lastIndexOf(".")).toLowerCase() + " ";

        if (supportedFileTypes.indexOf(sFiletype) > -1) {
          // do something with sFile
        }
      }
    }


    Handling URL drag/drop from Internet Explorer

    IE passes information slightly differently, URL's are passed as text, so you need to change the code slightly to handle these.  The URL you end up with is a string and to ensure we don't pick up files being drag/dropped, we just need to check if that string is null:

    function fileDragDropped() {
      var url = event.dataTransfer.getData("text");

      if (url != null) {
        // do something
      }
    }

    Putting it all together

    Combining both IE and file/drag drop together, your final function is:

    var supportedFileTypes = " .doc .xls .ppt ";
     
    function fileDragDropped() {
      var sFile, sFiletype;

      //is it a URL from IE?
      var url = event.dataTransfer.getData("text");

      if (url != null) {
        // do something with the url
      } else {
        for (var i=0; System.Shell.itemFromFileDrop(event.dataTransfer, i) != null; i++)
        {
          // get the file name and path
          sFile = System.Shell.itemFromFileDrop(event.dataTransfer, i).path;

          // get the extension
          sFiletype = " " + sFile.substring(sFile.lastIndexOf(".")).toLowerCase() + " ";

          // is the extension supported
          if (supportedFileTypes.indexOf(sFiletype) > -1) {
            //do something with sFile
          }
        }
      }
    }

    Gadget sound

    If you've tried using System.Sound you'll have realised just how useless it is. Not only is it monophonic, due to a bug it doesn't always play the sound. Why Microsoft choose this, over an implementation of DirectSound I don't understand. For Asteroids, I figured DirectSound was the only option, so I coded a COM wrapper.

    If you're trying to code a game, or musical instrument Gadget, you may find this handy. Here's the VB.NET code, I apologise for the lack of formatting, this stupid site strips out the indents.

    First the global definitions:

    Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As Integer) As IntPtr
    Private DSDevice As Microsoft.DirectX.DirectSound.Device
    Private Sound(0) As Microsoft.DirectX.DirectSound.SecondaryBuffer
    Private SoundFlags(0) As Microsoft.DirectX.DirectSound.BufferPlayFlags
    Private BufferDescriptions(0) As Microsoft.DirectX.DirectSound.BufferDescription


    The first problem you'll come across with DirectSound is it requires a HWND to link the sound to, without this you won't hear any sound. The reasoning behind this, is to silence games etc when you tab out of them. So, first we have to find the HWND of Sidebar. We do this by finding the window handle of Sidebar, which is called "SideBar_AppBarWindow", so we call Initialize("SideBar_AppBarWindow") which will find the window, create the DirectSound device and return True if it succeeded.

    Public Function Initialize(ByVal title As String) As Boolean
      On Error GoTo ErrOut
      Initialize = False

      Dim handle As System.IntPtr

      handle = FindWindow(title, 0)
      If Not handle.Equals(IntPtr.Zero) Then
        DSDevice = New Microsoft.DirectX.DirectSound.Device
        DSDevice.SetCooperativeLevel(handle, Microsoft.DirectX.DirectSound.CooperativeLevel.Priority)
        Initialize = True
        Exit Function
      End If

      ErrOut:
    End Function


    That's DirectSound initialised. Now we need to load sounds, which can be done via handle = LoadSound(<filename>, <loop>). LoadSound returns a sound handle, which you'll need to reference to play/stop sounds.

    Public Function LoadSound(ByVal sourceName As String, ByVal isLoop As Boolean) As Short
      Dim i As Short
      Dim index As Short

      'Search the sound array for any empty spaces
      index = -1
      For i = 0 To UBound(Sound)
        If Sound(i) Is Nothing Then 'If there is an empty space, us it
          index = i
          Exit For
        End If
      Next
      If index = -1 Then 'If there's no empty space, make a new spot
        index = UBound(Sound) + 1
        ReDim Preserve Sound(index)
        ReDim Preserve SoundFlags(index)
        ReDim Preserve BufferDescriptions(index)
      End If
      LoadSound = index

      If isLoop Then
        SoundFlags(index) = Microsoft.DirectX.DirectSound.BufferPlayFlags.Looping
      Else
        SoundFlags(index) = Microsoft.DirectX.DirectSound.BufferPlayFlags.Default
      End If

      BufferDescriptions(index) = New Microsoft.DirectX.DirectSound.BufferDescription
      BufferDescriptions(index).ControlVolume = True
      BufferDescriptions(index).ControlPan = True

      'Load the sound into the buffer
      Sound(index) = New Microsoft.DirectX.DirectSound.SecondaryBuffer(sourceName, BufferDescriptions(index), DSDevice)
    End Function


    To play a sound. PlaySound(<handle>):

    Public Sub PlaySound(ByVal index As Short)
      'Check to make sure there is a sound loaded in the specified buffer
      If Sound(index) Is Nothing Then Exit Sub

      Sound(index).SetCurrentPosition(0)
      Sound(index).Play(0, SoundFlags(index))
    End Sub

    To stop a sound, which is only really necessary for looped sounds. StopSound(<handle>):

    Public Sub StopSound(ByVal index As Short)
      'Check to make sure there is a sound loaded in the specified buffer
      If Sound(index) Is Nothing Then Exit Sub

      Sound(index).Stop()
    End Sub


    And finally, set the volume which in DirectSound goes from 0 (full volume) to -10000 (silent). SetVolume(<handle>, <volume>):

    Public Sub SetVolume(ByVal index As Short, ByVal vol As Integer)
      'Check to make sure there is a sound loaded in the specified buffer
      If Sound(index) Is Nothing Then Exit Sub

      Sound(index).Volume = vol
    End Sub


    You could go further and implement panning etc, but it wasn't required for my needs. It's simply a matter of copying SetVolume() and changing .Volume to .Pan

    The only thing left to implement is to cleanly close down DirectSound and Dispose of the sounds:

    Public Sub Terminate()
      Dim i As Short

      'Delete all of the sounds created
      For i = 0 To UBound(Sound)
        If Not Sound(i) Is Nothing Then Sound(i).Dispose()
      Next

      SoundFlags = Nothing
      Sound = Nothing
      DSDevice.Dispose()
    End Sub


    Once you've registered the DLL, your Gadget code will look something like:


    DSXLib = new ActiveXObject("<DLL class>");
    var gadgetPath = System.Gadget.path;
    var sounds = new Array(10);
    var sound = DSXLib.Initialize("SideBar_AppBarWindow");
    if (sound) {
      //load the sounds in to DirectSound buffers
      sounds[0] = DSXLib.LoadSound(gadgetPath + "\\sounds\\explode1.wav", false);
      sounds[1] = DSXLib.LoadSound(gadgetPath + "\\sounds\\explode2.wav", false);
      sounds[2] = DSXLib.LoadSound(gadgetPath + "\\sounds\\explode3.wav", false);
      sounds[3] = DSXLib.LoadSound(gadgetPath + "\\sounds\\fire.wav", false);
      sounds[4] = DSXLib.LoadSound(gadgetPath + "\\sounds\\life.wav", false);
      sounds[5] = DSXLib.LoadSound(gadgetPath + "\\sounds\\lsaucer.wav", true);
      sounds[6] = DSXLib.LoadSound(gadgetPath + "\\sounds\\sfire.wav", false);
      sounds[7] = DSXLib.LoadSound(gadgetPath + "\\sounds\\ssaucer.wav", true);
      sounds[8] = DSXLib.LoadSound(gadgetPath + "\\sounds\\thrust.wav", true);
      sounds[9] = DSXLib.LoadSound(gadgetPath + "\\sounds\\thumphi.wav", false);
      sounds[10] = DSXLib.LoadSound(gadgetPath + "\\sounds\\thumplo.wav", false);

      //set the volumes
      for(i=0; i<11; i++)
        DSXLib.SetVolume(sounds[i], -750);
    }

    ...
    // play a sound
    if (sound) DSXLib.PlaySound(sounds[3]);
    ...
    // clean up
    DSXLib.Terminate();

    March 09

    Using ActiveX DLL's in Gadgets

    I'll cover the specifics of creating a DLL suitable for Gadgets in a later article, for now I'll just cover how to register them.

    Assuming you have a .NET DLL, how do you register it? You can't use regasm as the user will not have enough rights to add it to the assembly, so you've got to resort to doing it manually. The first step is to get the registry entries from your DLL, which you can use regasm for (you can find regasm in "%windir%\Microsoft.NET\Framework\v2.0.50727"):

    regasm myfile.dll /codebase /regfile:myreg.reg


    This will result in a reg file something like:

    REGEDIT4

    [HKEY_CLASSES_ROOT\DSXLib.DSX]
    @="DSXLib.DSX"

    [HKEY_CLASSES_ROOT\DSXLib.DSX\CLSID]
    @="{3ADAA9CC-08D1-3745-8343-7BBDAD783F14}"

    [HKEY_CLASSES_ROOT\CLSID\{3ADAA9CC-08D1-3745-8343-7BBDAD783F14}]
    @="DSXLib.DSX"

    [HKEY_CLASSES_ROOT\CLSID\{3ADAA9CC-08D1-3745-8343-7BBDAD783F14}\InprocServer32]
    @="mscoree.dll"
    "ThreadingModel"="Both"
    "Class"="DSXLib.DSX"
    "Assembly"="DSXLib, Version=1.0.2579.29850, Culture=neutral, PublicKeyToken=null"
    "RuntimeVersion"="v2.0.50727"
    "CodeBase"="file:///C:/Users/Admin/AppData/Local/Microsoft/Windows Sidebar/Gadgets/Asteroids_dev.gadget/dsxlib.dll"

    [HKEY_CLASSES_ROOT\CLSID\{3ADAA9CC-08D1-3745-8343-7BBDAD783F14}\InprocServer32\1.0.2579.29850]
    "Class"="DSXLib.DSX"
    "Assembly"="DSXLib, Version=1.0.2579.29850, Culture=neutral, PublicKeyToken=null"
    "RuntimeVersion"="v2.0.50727"
    "CodeBase"="file:///C:/Users/Admin/AppData/Local/Microsoft/Windows Sidebar/Gadgets/Asteroids_dev.gadget/dsxlib.dll"

    [HKEY_CLASSES_ROOT\CLSID\{3ADAA9CC-08D1-3745-8343-7BBDAD783F14}\ProgId]
    @="DSXLib.DSX"

    [HKEY_CLASSES_ROOT\CLSID\{3ADAA9CC-08D1-3745-8343-7BBDAD783F14}\Implemented Categories\{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}]


    Take the red values and add them into the code below, which needs to be added into the HEAD section of your gadget.html file.

    Now, it's not as simply as adding the registry entries to HKCU as this fails if the user is running as an elevated Administrator. So you have to try to register them to HKCU and then register to HKLM if that fails. The code below will handle this for you:


    var axDllClass = "DSXLib.DSX";
    var axDllCLSID = "{3ADAA9CC-08D1-3745-8343-7BBDAD783F14}";
    var axDllAssembly = "DSXLib";
    var axDllVersion = "1.0.2579.29850";
    var axDllRuntimeVer = "v2.0.50727";
    var axDllFilename = "dsxlib.dll";

    var regRoot, dllOK, myDLL;
    if (!activateDLL("HKCU"))
        if (!activateDLL("HKLM"))
            System.Debug.outputString("Error creating ActiveX object");



    // Try to register the DLL
    function activateDLL(root) {
        regRoot = root;

        try{
        RegisterDLL();
            myDLL = new ActiveXObject(axDllClass);
            dllOK = true;
        } catch(err) {
           myDLL = null;
            dllOK = false;
            UnregisterDLL();
        }
        return dllOK;
    }


    // Register DLL
    function RegisterDLL() {
        try{
            var classRoot = regRoot + "\\Software\\Classes\\" + axDllClass + "\\";
            var clsidRoot = regRoot + "\\Software\\Classes\\CLSID\\" + axDllCLSID + "\\";
            var dllPath = System.Gadget.path.replace(RegExp("\\\\", "g"), "/") + "/" + axDllFilename;

            oShell.RegWrite(classRoot, axDllClass, "REG_SZ");
            oShell.RegWrite(classRoot + "CLSID\\", axDllCLSID, "REG_SZ");
            oShell.RegWrite(clsidRoot, axDllClass, "REG_SZ");
            oShell.RegWrite(clsidRoot + "InprocServer32\\", "mscoree.dll", "REG_SZ");
            oShell.RegWrite(clsidRoot + "InprocServer32\\ThreadingModel", "Both", "REG_SZ");
            oShell.RegWrite(clsidRoot + "InprocServer32\\Class", axDllClass, "REG_SZ");
            oShell.RegWrite(clsidRoot + "InprocServer32\\Assembly", axDllAssembly + ", Version=" + axDllVersion + ", Culture=neutral, PublicKeyToken=null", "REG_SZ");
            oShell.RegWrite(clsidRoot + "InprocServer32\\RuntimeVersion", axDllRuntimeVer, "REG_SZ");
            oShell.RegWrite(clsidRoot + "InprocServer32\\CodeBase", "file:///" + dllPath, "REG_SZ");
            oShell.RegWrite(clsidRoot + "InprocServer32\\" + axDllVersion + "\\Class", axDllClass, "REG_SZ");
            oShell.RegWrite(clsidRoot + "InprocServer32\\" + axDllVersion + "\\Assembly", axDllAssembly + ", Version=" + axDllVersion + ", Culture=neutral, PublicKeyToken=null", "REG_SZ");
            oShell.RegWrite(clsidRoot + "InprocServer32\\" + axDllVersion + "\\RuntimeVersion", axDllRuntimeVer, "REG_SZ");
            oShell.RegWrite(clsidRoot + "InprocServer32\\" + axDllVersion + "\\CodeBase", "file:///" + dllPath, "REG_SZ");
            oShell.RegWrite(clsidRoot + "ProgId\\", axDllClass, "REG_SZ");
            oShell.RegWrite(clsidRoot + "ProgId\\Implemented Categories\\{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}\\", "", "REG_SZ");
       
    } catch(err) {System.Debug.outputString("RegisterDLL: "+err.name+" - "+err.message)}
    }



    // Unregister DLL
    function UnregisterDLL() {
        var classRoot = regRoot + "\\Software\\Classes\\" + axDllClass + "\\";
        var clsidRoot = regRoot + "\\Software\\Classes\\CLSID\\" + axdllCLSID + "\\";

        try{       
            oShell.RegDelete(clsidRoot + "ProgId\\Implemented Categories\\{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}\\");
            oShell.RegDelete(clsidRoot + "ProgId\\Implemented Categories\\");
            oShell.RegDelete(clsidRoot + "ProgId\\");
            oShell.RegDelete(clsidRoot + "InprocServer32\\" + axDllVersion + "\\");
            oShell.RegDelete(clsidRoot + "InprocServer32\\");
            oShell.RegDelete(clsidRoot);
            oShell.RegDelete(classRoot + "CLSID\\");
            oShell.RegDelete(classRoot);
       
    } catch(err) {System.Debug.outputString("UnregisterDLL: "+err.name+" - "+err.message)}
    }


    In your code, you can now check if the DLL registered correctly by checking dllOK and reference it via myDLL. eg.

    if(dllOK) myDLL.PlaySound(...);


    The only thing you need to do now, is remove the assembly when the Gadget is removed so you're not leaving any rogue registry values. This you do by simply adding an unload event to the gadget.html BODY tag:

    <BODY ... onunload="UnregisterDLL()">