microsoft / Win2D

Win2D is an easy-to-use Windows Runtime API for immediate mode 2D graphics rendering with GPU acceleration. It is available to C#, C++ and VB developers writing apps for the Windows Universal Platform (UWP). It utilizes the power of Direct2D, and integrates seamlessly with XAML and CoreWindow.
http://microsoft.github.io/Win2D
Other
1.8k stars 285 forks source link

Why are the rectangles drawn with Win2D smaller than XAML ListView items ? #399

Closed octav84 closed 8 years ago

octav84 commented 8 years ago

Hi. I have a CanvasVirtualControl rendering a table made of cells 150 units in length and 40 units in height. I need to align this table with a horizontal rendering list which also consists of elements of 150 X 40 units.

Problem is that the cells rendered with Win2D are slightly smaller in length. When scrolling horizontally all 256 columns, this problem becomes obvious. At the end of the scroll, they are clearly out of sync, the XAML horizontal list is longer by a full 2/3 cell width than the grid generated with Win2D. The XAML horizontal list acts as a header for the Win2D grid, so the length should be exactly the same.

This is the XAML:

                         <ListView Margin="0"
     ItemContainerStyle="{StaticResource        LabResultsValuesListHeaderItemTemplate}"
                          ItemTemplate="{StaticResource ValuesHeaderTemplate}"
                          ItemsSource="{Binding LabResultsDetailsList.Dates}"
                          Padding="0">
                    <ListView.ItemsPanel>
                        <ItemsPanelTemplate>
                            <ItemsStackPanel Margin="0" Orientation="Horizontal"/>
                        </ItemsPanelTemplate>
                    </ListView.ItemsPanel>
                </ListView>

                <ScrollViewer x:Name="CanvasScroll"
                              Grid.Row="1"
                              HorizontalScrollBarVisibility="Disabled"
                              VerticalScrollBarVisibility="Auto"
                              VerticalScrollMode="Enabled"
                              ZoomMode="Disabled">
                    <canvas:CanvasVirtualControl x:Name="LabResultsCanvas"
                                                 ClearColor="White"
                                                 CreateResources="LabResultsCanvas_CreateResources"
                                                 RegionsInvalidated="OnRegionsInvalidated"
                                                 Tapped="LabResultsCanvas_OnTapped" />
                </ScrollViewer>

This is the Win2D Rendering:

            private void CalculateCanvasDimmensions()
     {
        LabResultsCanvas.Width = LabResultsViewModel.CanvasMatrix[0].Length * NumericConstants.LabResultCellWidth;
        LabResultsCanvas.Height = LabResultsViewModel.CanvasMatrix.GetLength(0) * NumericConstants.LabResultCellHeight;
    }

    private void OnRegionsInvalidated(CanvasVirtualControl sender, CanvasRegionsInvalidatedEventArgs args)
    {
        if (!isDirty)
        {
            CalculateCanvasDimmensions();
            isDirty = true;
        }

        foreach (var region in args.InvalidatedRegions)
        {
            DrawRegion(sender, region);
        }
        LabResultsCanvas.VerticalAlignment = VerticalAlignment.Top;
        LabResultsCanvas.HorizontalAlignment = HorizontalAlignment.Left;

        Thickness margin = LabResultsCanvas.Margin;
        margin.Top = -1 * NumericConstants.LabResultCellHeight;
        LabResultsCanvas.Margin = margin;
    }

    CanvasTextFormat coordFormat = new CanvasTextFormat()
    {
        HorizontalAlignment = CanvasHorizontalAlignment.Center,
        VerticalAlignment = CanvasVerticalAlignment.Center,
        FontSize = 10
    };

    private void DrawRegion(CanvasVirtualControl sender, Rect region)
    {
        using (var ds = sender.CreateDrawingSession(region))
        {             
            int matrixWidth = LabResultsViewModel.CanvasMatrix[0].Length;
            int matrixHeight = LabResultsViewModel.CanvasMatrix.GetLength(0);

            var left = ((int)(region.X / NumericConstants.LabResultCellWidth) - 1) * NumericConstants.LabResultCellWidth;
            var top = ((int)(region.Y / NumericConstants.LabResultCellHeight) - 1) * NumericConstants.LabResultCellHeight;

            var right = ((int)((region.X + region.Width) / NumericConstants.LabResultCellWidth) + 1) * NumericConstants.LabResultCellWidth;
            var bottom = ((int)((region.Y + region.Height) / NumericConstants.LabResultCellHeight) + 1) * NumericConstants.LabResultCellHeight;

            for (int x = left; x <= right; x += NumericConstants.LabResultCellWidth)
            {
                for (int y = top; y <= bottom; y += NumericConstants.LabResultCellHeight)
                {
                    if (y / NumericConstants.LabResultCellHeight >= 0 && y / NumericConstants.LabResultCellHeight < matrixHeight &&
                        x / NumericConstants.LabResultCellWidth >= 0 && x / NumericConstants.LabResultCellWidth < matrixWidth)
                    {
                        var pos = new Vector2((float)x + NumericConstants.LabResultCellWidth / 2,
                                          (float)y + NumericConstants.LabResultCellHeight / 2);

                        ds.DrawRectangle(x, y, NumericConstants.LabResultCellWidth, NumericConstants.LabResultCellHeight, Colors.Black, 1);
                        LabResultCell labResultCell = LabResultsViewModel.CanvasMatrix[y / NumericConstants.LabResultCellHeight][x / NumericConstants.LabResultCellWidth];

                        switch (labResultCell.Type)
                        {
                            case LabResultCellType.Header:
                                ds.DrawText(
                                    labResultCell.Date.ToString(),
                                    pos,
                                    Colors.Black,
                                    coordFormat);
                                break;
                            case LabResultCellType.Value:
                                if (labResultCell.LabResult == null)
                                {
                                    ds.DrawText(
                                    "--",
                                    pos,
                                    Colors.Black,
                                    coordFormat);
                                }
                                else
                                {
                                    ds.DrawText(
                                    labResultCell.LabResult.Value,
                                    pos,
                                    Colors.Black,
                                    coordFormat);
                                }
                                break;
                            default: throw new NotImplementedException();
                        }
                    }
                }
            }
        }
    }

This is the constants class:

   public static class NumericConstants
{
    public const int LabResultCellHeight = 40;
    public const int LabResultCellWidth = 150;
}

This is the ViewModel for the XAML page containing the CanvasMatrix:

     using Cerner.Pulse.Common.Models;
     using Cerner.Pulse.UI.Services.LabResultsService;
     using Cerner.Pulse.UI.Util;
    using Cerner.Pulse.UI.Util.ReviewUtil;
   using Cerner.Pulse.UI.Views.ReviewViews;
  using GalaSoft.MvvmLight.Command;
 using GalaSoft.MvvmLight.Ioc;
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Threading.Tasks;
 using Windows.ApplicationModel.Resources;
 using Windows.UI.Xaml.Controls;
 using Windows.UI.Xaml.Data;

namespace Cerner.Pulse.UI.ViewModels.LabResults
{
public class LabResultsViewModel : GenericViewModel
{
    private string filtersButtonText;
    private string selectedCategoryName;

    private int matrixHeight = 0;

    private RelayCommand loadLabResultsCommand;
    private RelayCommand<object> navigateToLabResultsDetailsForPhonePageCommand;
    private RelayCommand<ListView> bindListViewCommand;
    private RelayCommand navigateToFiltersPageCommand;

    private LabResultsDTO labResultsDetailsList;
    private CollectionViewSource labResultsDetailsCollection = new CollectionViewSource();
    private ObservableCollection<GroupInfo> labResultGroups = new ObservableCollection<GroupInfo>();

    private ILabResultsService LabResultsService => SimpleIoc.Default.GetInstance<ILabResultsService>();
    private ListView labResultsDetailsListView;
    public LabResultCell[][] CanvasMatrix { get; set; }
    private LabResultsFilterModel Filters { get; set; }
    private IComparer<LabResultsDTO.LabResult> LabResultsComparer;

    public string SelectedCategoryName
    {
        get
        {
            return selectedCategoryName;
        }
        set
        {
            if (selectedCategoryName != null && selectedCategoryName.Equals(value))
                return;
            selectedCategoryName = value;
            RaisePropertyChanged();
        }
    }
    public Dictionary<string, long> SectionsIdsDictionary { get; set; }

    public LabResultsDTO LabResultsDetailsList
    {
        get
        {
            return labResultsDetailsList;
        }
        set
        {
            labResultsDetailsList = value;
            RaisePropertyChanged();
        }
    }

    public CollectionViewSource LabResultsDetailsCollection
    {
        get
        {
            return labResultsDetailsCollection;
        }
        set
        {
            labResultsDetailsCollection = value;
            RaisePropertyChanged();
        }
    }

    public RelayCommand LoadLabResultsCommand
    {
        get
        {
            return loadLabResultsCommand ?? (loadLabResultsCommand = new RelayCommand(async () =>
            {
                await LoadLabResults();
            }));
        }
    }

    public RelayCommand<ListView> BindListViewCommand
    {
        get
        {
            return bindListViewCommand ?? (bindListViewCommand = new RelayCommand<ListView>((listView) =>
            {
                this.labResultsDetailsListView = listView;
            }));
        }
    }

    public RelayCommand<object> NavigateToLabResultsDetailsForPhonePageCommand
    {
        get
        {
            return navigateToLabResultsDetailsForPhonePageCommand ??
                (navigateToLabResultsDetailsForPhonePageCommand = new RelayCommand<object>
                    (parameter => NavigateToLabResultsDetailsForPhonePage(parameter)));
        }
    }

    public RelayCommand NavigateToFiltersPageCommand
    {
        get
        {
            return navigateToFiltersPageCommand ??
                (navigateToFiltersPageCommand = new RelayCommand(() => NavigateToFiltersPage()));
        }
    }

    public LabResultsViewModel()
    {
        PageTitle = ResourceLoader.GetForCurrentView().GetString("laboratoryResults");
        LeftSideButtonText = ResourceLoader.GetForCurrentView().GetString("back");
        FiltersButtonText = ResourceLoader.GetForCurrentView().GetString("filters");
        MessengerInstance.Register<LabResultsFilterModel>(this, (message) => Filters = message);

        LabResultsComparer = new DateTimeComparer();
    }

    public string FiltersButtonText
    {
        get
        {
            return filtersButtonText;
        }
        private set
        {
            if (filtersButtonText != null && filtersButtonText.Equals(value))
                return;

            filtersButtonText = value;
            RaisePropertyChanged();
        }
    }

    public async Task LoadLabResults()
    {
        try
        {
            if (!IsLoading)
            {
                IsLoading = true;
                LabResultsDetailsCollection = new CollectionViewSource();

                LabResultsDetailsList = await LabResultsService.GetDetailsAsync(SelectedPatient.Id, Filters);

                //if (Filters == null)
                //  SectionsIdsDictionary = GetSectionIdsDictionary();

                GroupLabResults();
            }
        }
        catch (Exception ex)
        {
            LogService.LogException(ex);
            DialogService.ShowMessage(ex.Message);
        }
        finally
        {
            IsLoading = false;
        }

    }

    private void GroupLabResults()
    {
        labResultGroups = new ObservableCollection<GroupInfo>();
        matrixHeight = 0; // to prevent bugs when navigationg again to LabResultsPage
        foreach (LabResultsDTO.ResultGroup group in LabResultsDetailsList.Groups)
        {
            GroupInfo labResultInfo = new GroupInfo { Key = group.Name };
            matrixHeight++;
            foreach (LabResultsDTO.ResultType type in group.Types)
            {
                labResultInfo.Items.Add(type);
                matrixHeight++;
            }
            labResultGroups.Add(labResultInfo);
        }

        LabResultsDetailsCollection.IsSourceGrouped = true;
        LabResultsDetailsCollection.ItemsPath = new Windows.UI.Xaml.PropertyPath("Items");
        LabResultsDetailsCollection.Source = labResultGroups;

        CreateCanvasMatrix();
        // TODO - need to scroll both lists
    }

    private void CreateCanvasMatrix()
    {
        ObservableCollection<GroupInfo> labResults = LabResultsDetailsCollection.Source as ObservableCollection<GroupInfo>;
        if (labResults == null)
            return;

        CanvasMatrix = new LabResultCell[matrixHeight][];
        int x = 0;
        int matrixWidth = LabResultsDetailsList.Dates.Count;

        foreach (GroupInfo groupInfo in labResults)
        {
            CanvasMatrix[x] = new LabResultCell[matrixWidth];
            for (int j = 0; j < matrixWidth; j++)
            {
                LabResultCell cell = new LabResultCell
                {
                    Type = LabResultCellType.Header,
                    Date = LabResultsDetailsList.Dates[j]
                };
                CanvasMatrix[x][j] = cell;
            }
            x++;

            foreach (LabResultsDTO.ResultType type in groupInfo.Items)
            {
                CanvasMatrix[x] = new LabResultCell[matrixWidth];
                for (int k = 0; k < matrixWidth; k++)
                {
                    LabResultsDTO.LabResult findLabResult = type.Results.Find(l => l.Date.Equals(LabResultsDetailsList.Dates[k]));
                    if (findLabResult != null)
                    {
                        LabResultCell cell = new LabResultCell
                        {
                            Type = LabResultCellType.Value,
                            LabResult = findLabResult
                        };
                        CanvasMatrix[x][k] = cell;
                    }
                    else
                    {
                        LabResultCell cell = new LabResultCell
                        {
                            Type = LabResultCellType.Value
                        };
                        CanvasMatrix[x][k] = cell;
                    }
                }
                x++;
            }
        }

    }

    private void NavigateToLabResultsDetailsForPhonePage(object resultsTypeData)
    {
        NavigationService.NavigateToApplicationFrame<object>
            (ApplicationFrame.ReviewFrame, typeof(LabResultsDetailsForPhonePage), resultsTypeData);
    }

    private void NavigateToFiltersPage()
    {
        NavigationService.NavigateToApplicationFrame
            (
                ApplicationFrame.ReviewFrame,
                typeof(LabResultsFiltersPage),
                SectionsIdsDictionary
            );
    }

    //private Dictionary<string, long> GetSectionIdsDictionary()
    //{
    //  return LabResultsDetailsList.Select(f =>
    //  new
    //  {
    //      Key = f.LabResultGroup.Name,
    //      TypeId = f.LabResultGroup.Id
    //  })
    //  .Distinct()
    //  .ToDictionary(f => f.Key, f => f.TypeId);
    //}
}

}

shawnhar commented 8 years ago

I wonder could this be a rounding behavior difference? Unless you are working in exactly integer screen pixel units (which isn't likely to be the case if you're running on a non-96-dpi display) XAML will do all kinds of smarts to snap UI elements to screen pixel locations.

Rather than trying to duplicate the exact layout calculations that XAML is doing for your Win2D rendering, I think a more robust approach would be to query XAML for the position of each element.

This seems like a good way to do that: http://stackoverflow.com/questions/12161584/absolute-coordinates-of-uielement-in-winrt

octav84 commented 8 years ago

You mean that I need to draw every cell using the X coordinates that the XAML ListView items have ? Isn't there a better way to do this ?

octav84 commented 8 years ago

Found the solution - in the OnRegionsInvalidated event handler, I got the actual width I needed to set by dividing the headers actual width to it's number of items. Strangely, the XAML ListView had drawn 150.3999... pixels wide elements instead of 150 pixel ones.

octav84 commented 8 years ago

Actually, not even this was enough. Finally I took your advice and got the exact coordinates of the items I needed to align to and it worked perfectly. Thank you very much.