Jonathan 的个人资料Jon's Vista Gadgets Blog日志列表 工具 帮助

日志


3月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
}

3月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();

3月9日

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()">

Array shuffling

A user pointed out to me that the Shuffle code in Desktop Wallpaper wasn't very good and very kindly provided an example of how to extend Array functionality with Array prototyping. So here's two array extensions I coded to improve the shuffle:

// shuffle array
Array.prototype.shuffle = function() {
  var elements = this.length;

  // step through the array
  for (var j=0; j<elements; j++)
    this.swap(j, Math.floor(elements * Math.random()));

  return this;
}

// swap array elements
Array.prototype.swap = function(a, b) {
  var tmp = this[a];
  this[a] = this[b];
  this[b] = tmp;
}

To shuffle an array, simply use "<arrayname>.shuffle();" and to swap array elements over, use "<arrayname>.swap(<e1>, <e2>);"

Optimizing graphics in Gadgets

One thing I keep seeing in Gadgets, which really bugs me is graphics file sizes.  Natively saved PNG files from Paint/Photoshop are huge and very inefficient and with readily available tools can be shrunk considerably.  For example.

"Yahoo! Search" show this to good effect, they've stripped out half of the colour schemes due to the size.  In it's current form, the Gadget is 2.95mb in size.  Running it though a lossless PNG optimizer brings this down to 2.65mb - 10% smaller.

Another annoyance is thumbs.db files.  I've noticed lots of Gadgets (mainly commercial) where the packagers haven't bothered to remove these unnecessary files.  "Yahoo! Search" being another example.  With the PNG optimization and these files remove the Gadget comes down to 2.02mb - 31% smaller.

My advice, before you package your Gadget file.  Run all the PNG files through something like PNGOut.  The DOS version is free, or you can buy the Window version here.  Then make sure you don't have any thumbs.db files if you've created the images on XP.

Now this is just an example of bad packaging and doesn't show really large file reductions.  By optimizing Asteroids and Media Player, the Gadget file sizes were reduced by 50% in the case of Asteroids and 35% in the case of Media Player.