3 questions related to ImageList, ListView and ComboBox

  • Thread starter Thread starter Steve K.
  • Start date Start date
S

Steve K.

I have a need to display an icon that is associated with specific types of
objects I'm displaying in my UI. For example, "Sales Order", "Invoice",
etc - each of these would have a specific icon.

I have an enum that I use to identify these different document types and a
meta data object that represents the document type, it's description and an
icon. I use these document type meta data objects to build up my UI. I
needed to create an ImageList and populate it with images, to do this I
wanted to first allocate enough space in the Images collection to allow me
to set the ImageList's Images by index using my enum values for the index
keys.

Here is the code I ended up using to get things working:
<code>
public void
SetAvailableInboundDocumentTypes(List<InboundDocumentTypeViewMetaData>
typeMetaDatas)
{
for (int i = 0; i < typeMetaDatas.Count; i++)
{
_imageList_DocTypeIcons.Images.Add(typeMetaDatas[0].SmallIcon);
}

foreach (InboundDocumentTypeViewMetaData typeMetaData in typeMetaDatas)
{
_pmdComboBox_DocumentType.Items.Add( new
PMDComboBoxItem(typeMetaData.Description, typeMetaData.SmallIcon,
typeMetaData.Type) );

_imageList_DocTypeIcons.Images[(int)typeMetaData.Type] =
typeMetaData.SmallIcon;
}
}
</code>

You can see above that I'm first looping through the total number of
document types adding the same image just to allocated the space, then later
in the real loop I'm assigning by index using the document type enum value
for the index.

Looking at the ImageCollection object there isn't a constructor that will
take a size parameter and I don't see any method to allocate or set the
size.

My question: Is this the best way to accomplish what I want? I suspect I'm
missing some trick to allocate the ImageList collection to a specific size,
but maybe not?


Question #2: Icons drawing terrible in my ListView
As you can see above, I'm populating an ImageList for my ListView. The size
of the Images are 24x24. When I assigned the ImageList to the ListView in
the designer the rows height increased which was encouraging, told me that
the ListView is aware of the ImageList size and adjusting accordingly.
However, when the ListView renders the images they look terrible. I took a
screenshot and analyzed in Photoshop and the ListView images are the correct
size. There seems to be a strange dark blue artifact around the image.
Here is a zoomed in screenshot:
http://www.pmddirect.com/temp/poor 24x24 image quality.png

And the original, source image (zoomed in):
http://www.pmddirect.com/temp/zoomed in original icon.png

The source images are stored in my Resource file and are PNG files.


Question #3: Custom ComboBox to support images. I took a quick swing and
creating this often requested control. There are plenty of examples on the
web of how to achieve this and I followed them for the most part with one
exception: I didn't use an ImageList to store my images, but rather added
an Image property to my custom ComboBoxItem. Here is the code:
<code>
public class PMDComboBox : ComboBox
{
public PMDComboBox()
{
DrawMode = DrawMode.OwnerDrawFixed;
}

protected override void OnDrawItem(DrawItemEventArgs e)
{
e.DrawBackground();
e.DrawFocusRectangle();

PMDComboBoxItem item;

Rectangle bounds = e.Bounds;

item = this.Items[e.Index] as PMDComboBoxItem;
if(item.Image != null)
{
Point labelPosition = new Point(bounds.Left + item.Image.Width +
6, bounds.Top + 10);

e.Graphics.DrawImage(item.Image, bounds.Location);
e.Graphics.DrawString(item.Text, e.Font, new
SolidBrush(e.ForeColor), labelPosition);
}
else
{
e.Graphics.DrawString(item.Text, e.Font, new
SolidBrush(e.ForeColor), bounds.Location);
}

base.OnDrawItem(e);
}
}


public class PMDComboBoxItem
{
private Image _image;
private string _text;
private object _tag;

public PMDComboBoxItem()
{
}

public PMDComboBoxItem(string text)
{
_text = text;
}

public PMDComboBoxItem(string text, Image image, object tag)
: this(text)
{
_image = image;
_tag = tag;
}

public Image Image
{
get { return _image; }
set { _image = value; }
}

public string Text
{
get { return _text; }
set { _text = value; }
}

public object Tag
{
get { return _tag; }
set { _tag = value; }
}

public override string ToString()
{
if (_text != null)
{
return _text;
}

return base.ToString();
}
}
</code>

Pretty straightforward. It works.... but the rendered image is terribly
hideous!

Again with the screen shots:
http://www.pmddirect.com/temp/ComboBox image scaled up.png

I've explicitly set the ItemHeight of the ComboBox to 24 (the size of my
images). As you can see from the screenshot, the images are being scaled
for some reason. I've set a breakpoint in the paint method and verified
that the following:
- the source image resolution is 24x24
- DrawItemEventArgs.Graphics.Bounds are correct (24px high...)
- Page scale = 1.0
- DPI x,y = 96

Actually... I just figured out what the problem is. The Image should be
rendered 24x24, but IS being rendered at 32x32 (133% larger). As mentioned
above, the DPI of the Graphics context is 96 but my source image has a DPI
of 72 (96/72 = 1.333)

It would seem that in other contexts GDI is compensating for this and
handling it, but not in this one.

So now the challenge is how to correct this? The Graphics object's DPI
settings are read only, so I can modify them. I know I can redraw the Image
and calculate the pixel dimensions (physical * dpi) but this will be a bit
expensive to do in a paint method.

Anyone have any ideas?

OK, well that's it. I know it's a lot of questions, but hopefully there is
someone out there with experience in these areas that could save me some
time. I hope I've provided enough information.

-Steve
 
Answer to #3:
<code>
e.Graphics.DrawImage(item.Image, imagePosition.X, imagePosition.Y,
item.Image.Width, item.Image.Height);
</code>

Specifying the size seems to be all that is needed! Easy!!
:0)

Steve K. said:
I have a need to display an icon that is associated with specific types of
objects I'm displaying in my UI. For example, "Sales Order", "Invoice",
etc - each of these would have a specific icon.

I have an enum that I use to identify these different document types and a
meta data object that represents the document type, it's description and
an icon. I use these document type meta data objects to build up my UI.
I needed to create an ImageList and populate it with images, to do this I
wanted to first allocate enough space in the Images collection to allow me
to set the ImageList's Images by index using my enum values for the index
keys.

Here is the code I ended up using to get things working:
<code>
public void
SetAvailableInboundDocumentTypes(List<InboundDocumentTypeViewMetaData>
typeMetaDatas)
{
for (int i = 0; i < typeMetaDatas.Count; i++)
{
_imageList_DocTypeIcons.Images.Add(typeMetaDatas[0].SmallIcon);
}

foreach (InboundDocumentTypeViewMetaData typeMetaData in typeMetaDatas)
{
_pmdComboBox_DocumentType.Items.Add( new
PMDComboBoxItem(typeMetaData.Description, typeMetaData.SmallIcon,
typeMetaData.Type) );

_imageList_DocTypeIcons.Images[(int)typeMetaData.Type] =
typeMetaData.SmallIcon;
}
}
</code>

You can see above that I'm first looping through the total number of
document types adding the same image just to allocated the space, then
later in the real loop I'm assigning by index using the document type enum
value for the index.

Looking at the ImageCollection object there isn't a constructor that will
take a size parameter and I don't see any method to allocate or set the
size.

My question: Is this the best way to accomplish what I want? I suspect
I'm missing some trick to allocate the ImageList collection to a specific
size, but maybe not?


Question #2: Icons drawing terrible in my ListView
As you can see above, I'm populating an ImageList for my ListView. The
size of the Images are 24x24. When I assigned the ImageList to the
ListView in the designer the rows height increased which was encouraging,
told me that the ListView is aware of the ImageList size and adjusting
accordingly. However, when the ListView renders the images they look
terrible. I took a screenshot and analyzed in Photoshop and the ListView
images are the correct size. There seems to be a strange dark blue
artifact around the image.
Here is a zoomed in screenshot:
http://www.pmddirect.com/temp/poor 24x24 image quality.png

And the original, source image (zoomed in):
http://www.pmddirect.com/temp/zoomed in original icon.png

The source images are stored in my Resource file and are PNG files.


Question #3: Custom ComboBox to support images. I took a quick swing and
creating this often requested control. There are plenty of examples on
the web of how to achieve this and I followed them for the most part with
one exception: I didn't use an ImageList to store my images, but rather
added an Image property to my custom ComboBoxItem. Here is the code:
<code>
public class PMDComboBox : ComboBox
{
public PMDComboBox()
{
DrawMode = DrawMode.OwnerDrawFixed;
}

protected override void OnDrawItem(DrawItemEventArgs e)
{
e.DrawBackground();
e.DrawFocusRectangle();

PMDComboBoxItem item;

Rectangle bounds = e.Bounds;

item = this.Items[e.Index] as PMDComboBoxItem;
if(item.Image != null)
{
Point labelPosition = new Point(bounds.Left + item.Image.Width
+ 6, bounds.Top + 10);

e.Graphics.DrawImage(item.Image, bounds.Location);
e.Graphics.DrawString(item.Text, e.Font, new
SolidBrush(e.ForeColor), labelPosition);
}
else
{
e.Graphics.DrawString(item.Text, e.Font, new
SolidBrush(e.ForeColor), bounds.Location);
}

base.OnDrawItem(e);
}
}


public class PMDComboBoxItem
{
private Image _image;
private string _text;
private object _tag;

public PMDComboBoxItem()
{
}

public PMDComboBoxItem(string text)
{
_text = text;
}

public PMDComboBoxItem(string text, Image image, object tag)
: this(text)
{
_image = image;
_tag = tag;
}

public Image Image
{
get { return _image; }
set { _image = value; }
}

public string Text
{
get { return _text; }
set { _text = value; }
}

public object Tag
{
get { return _tag; }
set { _tag = value; }
}

public override string ToString()
{
if (_text != null)
{
return _text;
}

return base.ToString();
}
}
</code>

Pretty straightforward. It works.... but the rendered image is terribly
hideous!

Again with the screen shots:
http://www.pmddirect.com/temp/ComboBox image scaled up.png

I've explicitly set the ItemHeight of the ComboBox to 24 (the size of my
images). As you can see from the screenshot, the images are being scaled
for some reason. I've set a breakpoint in the paint method and verified
that the following:
- the source image resolution is 24x24
- DrawItemEventArgs.Graphics.Bounds are correct (24px high...)
- Page scale = 1.0
- DPI x,y = 96

Actually... I just figured out what the problem is. The Image should be
rendered 24x24, but IS being rendered at 32x32 (133% larger). As
mentioned above, the DPI of the Graphics context is 96 but my source image
has a DPI of 72 (96/72 = 1.333)

It would seem that in other contexts GDI is compensating for this and
handling it, but not in this one.

So now the challenge is how to correct this? The Graphics object's DPI
settings are read only, so I can modify them. I know I can redraw the
Image and calculate the pixel dimensions (physical * dpi) but this will be
a bit expensive to do in a paint method.

Anyone have any ideas?

OK, well that's it. I know it's a lot of questions, but hopefully there
is someone out there with experience in these areas that could save me
some time. I hope I've provided enough information.

-Steve
 
I guess typing all that out stirred something loose in my head! ;0)

#2 Answer: Set the colordepth for the ImageList to match the images you are
supplying. In my case it was set to 8, changing to 32 fixed things up.
-Steve

Steve K. said:
I have a need to display an icon that is associated with specific types of
objects I'm displaying in my UI. For example, "Sales Order", "Invoice",
etc - each of these would have a specific icon.

I have an enum that I use to identify these different document types and a
meta data object that represents the document type, it's description and
an icon. I use these document type meta data objects to build up my UI.
I needed to create an ImageList and populate it with images, to do this I
wanted to first allocate enough space in the Images collection to allow me
to set the ImageList's Images by index using my enum values for the index
keys.

Here is the code I ended up using to get things working:
<code>
public void
SetAvailableInboundDocumentTypes(List<InboundDocumentTypeViewMetaData>
typeMetaDatas)
{
for (int i = 0; i < typeMetaDatas.Count; i++)
{
_imageList_DocTypeIcons.Images.Add(typeMetaDatas[0].SmallIcon);
}

foreach (InboundDocumentTypeViewMetaData typeMetaData in typeMetaDatas)
{
_pmdComboBox_DocumentType.Items.Add( new
PMDComboBoxItem(typeMetaData.Description, typeMetaData.SmallIcon,
typeMetaData.Type) );

_imageList_DocTypeIcons.Images[(int)typeMetaData.Type] =
typeMetaData.SmallIcon;
}
}
</code>

You can see above that I'm first looping through the total number of
document types adding the same image just to allocated the space, then
later in the real loop I'm assigning by index using the document type enum
value for the index.

Looking at the ImageCollection object there isn't a constructor that will
take a size parameter and I don't see any method to allocate or set the
size.

My question: Is this the best way to accomplish what I want? I suspect
I'm missing some trick to allocate the ImageList collection to a specific
size, but maybe not?


Question #2: Icons drawing terrible in my ListView
As you can see above, I'm populating an ImageList for my ListView. The
size of the Images are 24x24. When I assigned the ImageList to the
ListView in the designer the rows height increased which was encouraging,
told me that the ListView is aware of the ImageList size and adjusting
accordingly. However, when the ListView renders the images they look
terrible. I took a screenshot and analyzed in Photoshop and the ListView
images are the correct size. There seems to be a strange dark blue
artifact around the image.
Here is a zoomed in screenshot:
http://www.pmddirect.com/temp/poor 24x24 image quality.png

And the original, source image (zoomed in):
http://www.pmddirect.com/temp/zoomed in original icon.png

The source images are stored in my Resource file and are PNG files.


Question #3: Custom ComboBox to support images. I took a quick swing and
creating this often requested control. There are plenty of examples on
the web of how to achieve this and I followed them for the most part with
one exception: I didn't use an ImageList to store my images, but rather
added an Image property to my custom ComboBoxItem. Here is the code:
<code>
public class PMDComboBox : ComboBox
{
public PMDComboBox()
{
DrawMode = DrawMode.OwnerDrawFixed;
}

protected override void OnDrawItem(DrawItemEventArgs e)
{
e.DrawBackground();
e.DrawFocusRectangle();

PMDComboBoxItem item;

Rectangle bounds = e.Bounds;

item = this.Items[e.Index] as PMDComboBoxItem;
if(item.Image != null)
{
Point labelPosition = new Point(bounds.Left + item.Image.Width
+ 6, bounds.Top + 10);

e.Graphics.DrawImage(item.Image, bounds.Location);
e.Graphics.DrawString(item.Text, e.Font, new
SolidBrush(e.ForeColor), labelPosition);
}
else
{
e.Graphics.DrawString(item.Text, e.Font, new
SolidBrush(e.ForeColor), bounds.Location);
}

base.OnDrawItem(e);
}
}


public class PMDComboBoxItem
{
private Image _image;
private string _text;
private object _tag;

public PMDComboBoxItem()
{
}

public PMDComboBoxItem(string text)
{
_text = text;
}

public PMDComboBoxItem(string text, Image image, object tag)
: this(text)
{
_image = image;
_tag = tag;
}

public Image Image
{
get { return _image; }
set { _image = value; }
}

public string Text
{
get { return _text; }
set { _text = value; }
}

public object Tag
{
get { return _tag; }
set { _tag = value; }
}

public override string ToString()
{
if (_text != null)
{
return _text;
}

return base.ToString();
}
}
</code>

Pretty straightforward. It works.... but the rendered image is terribly
hideous!

Again with the screen shots:
http://www.pmddirect.com/temp/ComboBox image scaled up.png

I've explicitly set the ItemHeight of the ComboBox to 24 (the size of my
images). As you can see from the screenshot, the images are being scaled
for some reason. I've set a breakpoint in the paint method and verified
that the following:
- the source image resolution is 24x24
- DrawItemEventArgs.Graphics.Bounds are correct (24px high...)
- Page scale = 1.0
- DPI x,y = 96

Actually... I just figured out what the problem is. The Image should be
rendered 24x24, but IS being rendered at 32x32 (133% larger). As
mentioned above, the DPI of the Graphics context is 96 but my source image
has a DPI of 72 (96/72 = 1.333)

It would seem that in other contexts GDI is compensating for this and
handling it, but not in this one.

So now the challenge is how to correct this? The Graphics object's DPI
settings are read only, so I can modify them. I know I can redraw the
Image and calculate the pixel dimensions (physical * dpi) but this will be
a bit expensive to do in a paint method.

Anyone have any ideas?

OK, well that's it. I know it's a lot of questions, but hopefully there
is someone out there with experience in these areas that could save me
some time. I hope I've provided enough information.

-Steve
 
Peter Duniho said:
[...]
My question: Is this the best way to accomplish what I want? I suspect
I'm
missing some trick to allocate the ImageList collection to a specific
size,
but maybe not?

I think it's a mistake for you to even be worrying about this. The
internal allocation and representation of the images is an implementation
detail. At best, your code is depending on something that can easily
change in the future, and at worst it's making incorrect assumptions about
the behavior of the ImageList class and causing unnecessary overhead.

Hi Peter,
I think you misunderstood what I'm trying to do. I don't want to change the
way anything works, I just want to assign my images like this:
ImageList.Images[0] = imageA;
ImageList.Images[1] = imageC;
ImageList.Images[2] = imageB;

In order to access the array elements like that the array needs to be
allocated. I was hoping to find something like:
ImageList.Images = new ImageCollection(3);
Just add images one by one as you can generate/load them and forget trying
to manipulate the ImageList class into doing something that it doesn't
support as a public API.

The point was storing the images in specific indexes, if I just stack them
up one after another I can't access them by using my docTypeId value.

Thanks for the response.
I saw a response form you about my XML serialization issues,but when I
looked for it later (I found my 2 first threads) your responses were not
visible. I just checked again and can't find them. Anyway, I didn't mean
to not respond with thanks, I just couldn't find them. If I remember
correctly your suggestion was to serialize the base type or something along
those lines. I will try a different news clients and see if I can maybe get
to the message.

Take care,
Steve
Question #2: Icons drawing terrible in my ListView
[...] There seems to be a strange dark blue artifact around the image.
Here is a zoomed in screenshot:
http://www.pmddirect.com/temp/poor 24x24 image quality.png

And the original, source image (zoomed in):
http://www.pmddirect.com/temp/zoomed in original icon.png

It's hard to know for sure without a concise-but-complete code example.
However, the blue looks to me like something related to selection of the
item in the list, and the artifact may possibly be related to the
transparency in the original image.

This was solved in another post. I tried to respond ASAP to hopefully
prevent anyone from spending time answering. Sorry for not catching you in
time.
 
Peter said:
I think you misunderstood what I'm trying to do.
Possibly.

I don't want to change the
way anything works, I just want to assign my images like this:
ImageList.Images[0] = imageA;
ImageList.Images[1] = imageC;
ImageList.Images[2] = imageB;

In order to access the array elements like that the array needs to be
allocated. I was hoping to find something like:
ImageList.Images = new ImageCollection(3);
[...]

I've figured out what it is you _really_ want to do, which is
that you want to use the InboundDocumentTypeViewMetaData.Type property
as a key for each image in your ImageList.

Yes, that is correct.

But I still don't think the way you're doing it is really the right
way. Given that the code you posted assumes that the
InboundDocumentTypeViewMetaData.Type property has a 1-to-1 relationship
between the number of elements in your "typeMetaDatas" collection, it
seems to me that you might as well make the Type property an index into
that collection (i.e. make sure that the elements in that collection are
ordered according to the Type property of each element). With that,
then you can just call the Add() method for each new image in the list
within the single loop, rather than wasting time with the previous loop.

This is an interesting approach, I hadn't thought of this. The main
downside I see is that this implementation depends on the objects being
ordered. This is not a difficult thing to enforce, but could be broken
fairly easy if someone were to introduce a new item in the collection.

Let me show some code, just to make sure I'm not confusing things with
my written explanations.

<code>

// Here is the method that returns the collection
public List<InboundDocumentTypeViewMetaData>
GetSupportedDocumentTypesMetaData()
{
List<InboundDocumentTypeViewMetaData> typeMetaDatas =
new List<InboundDocumentTypeViewMetaData>();

typeMetaDatas.Add(new
InboundDocumentTypeViewMetaData(Resource.DMEDocTypeIcon_UNK_24,
Resource.DMEDocTypeIcon_UNK_48, "Unknown", InboundDocumentType.Unknown) );
typeMetaDatas.Add(new
InboundDocumentTypeViewMetaData(Resource.DMEDocTypeIcon_PFS_24,
Resource.DMEDocTypeIcon_PFS_48, "ET Face Sheet (100)",
InboundDocumentType.FaceSheet));
typeMetaDatas.Add(new
InboundDocumentTypeViewMetaData(Resource.DMEDocTypeIcon_PFS_24,
Resource.DMEDocTypeIcon_PFS_48, "Pre-Auth (Face Sheet)",
InboundDocumentType.FaceSheet));
typeMetaDatas.Add(new
InboundDocumentTypeViewMetaData(Resource.DMEDocTypeIcon_EOB_24,
Resource.DMEDocTypeIcon_EOB_48, "EOB/Remittance", InboundDocumentType.EOB));
typeMetaDatas.Add(new
InboundDocumentTypeViewMetaData(Resource.DMEDocTypeIcon_EOB_24,
Resource.DMEDocTypeIcon_EOB_48, "CMN - Certificate of Medical
Necessity", InboundDocumentType.CMN));
typeMetaDatas.Add(new
InboundDocumentTypeViewMetaData(Resource.DMEDocTypeIcon_RXR_24,
Resource.DMEDocTypeIcon_RXR_48, "RxR - Prescription Request",
InboundDocumentType.RxR));
typeMetaDatas.Add(new
InboundDocumentTypeViewMetaData(Resource.DMEDocTypeIcon_901_24,
Resource.DMEDocTypeIcon_901_48, "Inv. Xfer Req. (901)",
InboundDocumentType.InventoryTransferRequest));
typeMetaDatas.Add(new
InboundDocumentTypeViewMetaData(Resource.DMEDocTypeIcon_900_24,
Resource.DMEDocTypeIcon_900_48, "Inv. Xfer Agreement (900)",
InboundDocumentType.InventoryTransferAgreement));

return typeMetaDatas;
}

public class InboundDocumentTypeViewMetaData
{
private Image _smallIcon;
private Image _mediumIcon;
private string _description;
private InboundDocumentType _type;

internal InboundDocumentTypeViewMetaData(Image smallIcon, Image
mediumIcon, string description, InboundDocumentType type)
{
_smallIcon = smallIcon;
_mediumIcon = mediumIcon;
_description = description;
_type = type;
}

// [...] Properties snipped out
}

</code>

Long object names, I know.

As the InboundDocumentType enum is the driving force behind all of this,
it seems natural that a new document type would be appended to the enum
and that adding it to the end of the GetSupportedDocumentTypesMetaData()
would work nicely. It's just not guaranteed and it's something I need
to be aware or add decent comments along the lines of "Hey, if you add
additional items to this method to return, make sure they are in order"

Alternatively, if you have some resistance to acknowledging that
relationship, you could just use convert the Type to a string and use
the string as the key for the image being added (and of course, use the
Type converted to a string as the key to indicate which image in the
ImageList is for each item in your control).

I didn't notice that the Add() method takes a string key, too bad it's
not an object key - would be cleaner in my case. Still, thanks for
pointing that out, I missed it.

Basically, the code you've written currently makes an assumption that is
not actually enforced, nor is it readily detectable if the assumption is
broken (for example, the code will continue to run fine if your
"typeMetaDatas" collection winds up with more than one element with the
same Type property, but you'll wind up with the same image used for
multiple types).

Good point, this is a definite weakness in my current implementation.

You should either codify that assumption in a way that
ensures it won't be easily broken, or you should abandon the assumption
altogether and take advantage of keyed access to the image list.

I like the keyed approach the best, it is the safest and requires the
least amount of oversight and checking to enforce. It will "just work"
short of a total mess somewhere else.

I really wish the keys were objects, not because converting the Type
enum value to a string is a big deal when building up the list, but
rather I don't like having ToString() calls all over the place when I
want to fetch my Image.

Oh well, can't have it all, right? ;0)
Just my two cents. I just think that the code as designed is
inefficient and more likely to break than other designs would be.

You make good points and have made me think about some different things,
thanks for taking the time.

-Steve
 
Back
Top