Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ribbon Button: when passing the same icon file to the properties Icon and LargeIcon, the control doesn't seem to take the good icon frame #14

Closed
xinyingho opened this issue Nov 10, 2014 · 7 comments
Assignees
Milestone

Comments

@xinyingho
Copy link

I have an application with several ribbon buttons, to which I pass icon files to the properties Icon and LargeIcon.
The thing is that button controls don't seem to take the good icon frame for the property Icon.
For instance, I have an icon file, which have several frames from 16x16 to 64x64. When looking at the resulting ribbon button, it seems to always take the 32x32 frame and then resize this one to fit within the button instead of taking the 32x32 frame for LargeIcon and the 16x16 frame for Icon.
Well, it would be the ideal behaviour at 96dpi. But at higher dpi, buttons should try to grab frames with higher pixel sizes.

@xinyingho
Copy link
Author

I already modified the code for Fluent to be icon-aware at 96dpi. But then it's not dpi-aware yet, a useful feature in Windows 8.1. Here's the modified source:

<!-- example with Themes/Office2010/Controls/Button.xaml, simply add some ConverterParameters with the wanted pixel size at 96dpi wherever needed -->
<Setter Property="Content" TargetName="iconImage" Value="{Binding Icon, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static Converters:StaticConverters.ObjectToImageConverter}, ConverterParameter=16}"/>
<!-- I did the same with Themes/Office2010/Controls/RibbonGroupBox.xaml -->
// Fluent/Converters/ObjectToImageConverter.cs, nothing else needs to be modified code-side
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
    if (value is string)
    {
        var valueString = (string)value;
        Func<ImageSource> CreateImageSource = () => {
            if (parameter != null && valueString.EndsWith(".ico"))
            { // icon-aware code
                var decoder = BitmapDecoder.Create(
                    new Uri("pack://application:,,," + valueString, UriKind.RelativeOrAbsolute),
                    BitmapCreateOptions.DelayCreation | BitmapCreateOptions.IgnoreImageCache,
                    BitmapCacheOption.None
                );

                var result = decoder.Frames.SingleOrDefault(f => f.Width == System.Convert.ToDouble(parameter));
                if (result == default(BitmapFrame))
                    result = decoder.Frames.OrderBy(f => f.Width).Last();
                return result;
            }
            else
                // original code
                return new BitmapImage(
                    new Uri(value as string, UriKind.RelativeOrAbsolute),
                    new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore)
                );
        };

        var image = new Image
        {
            Source = CreateImageSource()
        };

        return image;
    }

    var imageSource = value as ImageSource;

    if (imageSource != null)
    {
        var image = new Image
        {
            Source = imageSource
        };
        return image;
    }
    return value;
}

@xinyingho
Copy link
Author

Ok, here's my attempt to make Fluent partially dpi-aware (i.e. with this, it still won't dynamically refresh button icons if moving the main window to another monitor with a different dpi):

// Fluent/Converters/ObjectToImageConverter.cs, I just modified the icon-aware section code
var decoder = BitmapDecoder.Create(
    new Uri("pack://application:,,," + valueString, UriKind.RelativeOrAbsolute),
    BitmapCreateOptions.DelayCreation | BitmapCreateOptions.IgnoreImageCache,
    BitmapCacheOption.None
);

// dpi.M11 = dpiX, dpi.M22 = dpiY
Matrix dpi = PresentationSource.FromVisual(Application.Current.MainWindow).CompositionTarget.TransformToDevice;

var result = decoder.Frames.SingleOrDefault(f => f.Width == System.Convert.ToDouble(parameter) * dpi.M11);
if (result == default(BitmapFrame))
    result = decoder.Frames.OrderBy(f => f.Width).Last();
return result;

It's really simple as dpi.M11 contains the required scaling (96dpi = 100% = 1.0, 120dpi = 125% = 1.25 and so on). But it actually doesn't work as expected because Fluent is letting WPF doing the scaling: With a LargeIcon declared as being 32x32 in the XAML source code, at 144dpi / 150% my code takes the 48x48 frame, Fluent then scales it down to 32x32, and then WPF scales the whole application up by 150%, resulting with the LargeIcon being back to 48x48. But this double scaling is actually quite destructive for the initial BitmapFrame...
The solution is probably to disable WPF automatic scaling by adding [assembly: System.Windows.Media.DisableDpiAwareness] and bind icon widths and heights to the current application dpi instead of hardcoding everything. Well, it would probably means to do this job for every UI element...

@xinyingho
Copy link
Author

Adding [assembly: System.Windows.Media.DisableDpiAwareness] is actually worse: everything (text, blocks...) is drawn at 96dpi and then scaled up to 144dpi while, without this assembly metadata, only icons are badly scaled.
I have no other ideas about how to fix bitmap scaling right now...

@xinyingho
Copy link
Author

It turned out that I was in the good direction. It's just that some icons didn't have every size. So following my code, buttons were taking the biggest size available (e.g. 256x256) and then WPF scaled this down to the target size (e.g. declared size: 16x16, target size at 144dpi: 24x24).
So I tweaked the code to take the closest bigger size frame when the requested size isn't available (e.g. 32x32):

var decoder = BitmapDecoder.Create(
    new Uri("pack://application:,,," + valueString, UriKind.RelativeOrAbsolute),
    BitmapCreateOptions.DelayCreation | BitmapCreateOptions.IgnoreImageCache,
    BitmapCacheOption.None
);

// dpi.M11 = dpiX, dpi.M22 = dpiY
Matrix dpi = PresentationSource.FromVisual(Application.Current.MainWindow).CompositionTarget.TransformToDevice;

var result = decoder.Frames.OrderBy(f => f.Width).Where(f => f.Width >= System.Convert.ToDouble(parameter) * dpi.M11).FirstOrDefault();
if (result == default(BitmapFrame))
    result = decoder.Frames.OrderBy(f => f.Width).Last();
return result;

Icons still aren't as sharp as they should be, at least when comparing with the rendering at 96dpi, but it's the best I could do. If any expert could come up and gives the definitive solution, I would be thankful for that.

@xinyingho
Copy link
Author

Last message before somebody reply to my monologue: my modified ObjectToImageConverter.cs upsets the WPF designer with an error "value cannot be null. missing parameter: v.". To fix this, simply ensure that Application.Current.MainWindow != null otherwise, instead of using dpi.M11, directly use 1.0.

@batzen
Copy link
Member

batzen commented Nov 11, 2014

I will try to have a look at this at the weekend.

@batzen batzen modified the milestone: 3.2.0 Nov 16, 2014
@batzen
Copy link
Member

batzen commented Dec 14, 2014

Could you do your changes in a fork and create a PR for this? Would be much easier for me.

@batzen batzen modified the milestones: 3.2.0, 3.3.0 Dec 15, 2014
@batzen batzen self-assigned this Jan 30, 2015
@batzen batzen closed this as completed in 993e5a2 Jan 30, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants