Turn your old Surface into into a large touchpad:

Have you got an old RT Surface 1 or 2 gathering dust and don't know what to do with it?

  • Create a UWP app to run on a Surface (ARM or x86), or any touchscreen Windows laptop. Could even recycle an Windows Mobile (8.1/10) phone.
  • Uses rounded boxes a touch keys.
  • Can remotely act as the UI for an app running on any system that supports Serial, Bluetooth(serial) or has network connectivity.  Azure IoT Hub is also envisaged as a communication medium.

Links

Index or Articles

Next: Json Configuration

Code Repository: (Github) djaus2/SurfPad

 

About

In what follows some of the coding techniques used will be presented.

 

Rounded Box

The intention is to create a grid of rounded boxes, each being a button. Each button to potentially have multiline text.

The first iteration involves having a border with a radius (to get the rounded corners) enclosing a multiline TextBlock:

            <Border BorderThickness="1" BorderBrush="Black" Background="LightGreen" 
                CornerRadius="5”
                <TextBlock Text="{Binding}"  TextAlignment="Center"  
                       TextWrapping="WrapWholeWords"
                       VerticalAlignment="Center" HorizontalAlignment="Center" />
            </Border >

A tapped event is then added to the TextBlock which does generate an event, but there is no animation. So need to replace it with a Button.

        <Border x:Name="Borderx" BorderThickness="1" BorderBrush="Black" Background="LightGreen" 
                Padding="0"
                CornerRadius="5>
            <Button x:Name="TheText"  
                       VerticalAlignment="Center" HorizontalAlignment="Center" 
                        Background="Transparent"
                        Tapped="TheText_Tapped" >
            </Button>
        </Border >

The problems with this are two-fold: The Button doesn’t fill the border and the text is not wrapped (multiline). The fix is to insert a TextBlock within the button (as its content):

        <Border BorderThickness="1" BorderBrush="Black" Background="LightGreen" 
                CornerRadius="5" 
                Width="120" Height="100">
            <Button x:Name="TheText"  
                       VerticalAlignment="Center" HorizontalAlignment="Center" 
                        Background="Transparent"
                        Tapped="TheText_Tapped" >
                <TextBlock Text="The quick brown fox jumps over the lazy dog."  TextAlignment="Center"  
                       TextWrapping="WrapWholeWords"
                       VerticalAlignment="Center" HorizontalAlignment="Center"/>
            </Button>
        </Border>

 

image

The Rounded Box Button

 

The Grid

As we want to programmatically add these buttons in a grid from information sent from the remote app, the Button needs to be implemented as a UserControl.

Initially the grid’s rows and columns were specified in the app’s MainPage XAML.

        <Grid.RowDefinitions>
            <RowDefinition Height="{Binding Source={StaticResource HeightSpace}}" />
            <RowDefinition Height="{Binding Source={StaticResource HeightPad}}" />
            <RowDefinition Height="{Binding Source={StaticResource HeightSpace}}" />
            <RowDefinition Height="{Binding Source={StaticResource HeightPad}}" />
           .....
           .....
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="{Binding Source={StaticResource WidthSpace}}" />
            <ColumnDefinition Width="{Binding Source={StaticResource WidthPad}}" />
            <ColumnDefinition Width="{Binding Source={StaticResource WidthSpace}}" />
            <ColumnDefinition Width="{Binding Source={StaticResource WidthPad}}" />
           .....
           .....
        </Grid.ColumnDefinitions>

The grid has a small blank row between each Button row and a small blank column between each Button column ( more on grid spacing later). Grid dimensions are specified in as resources within the XAML page:

    <Page.Resources>
        <Style TargetType="RowDefinition" x:Key="HeightSpace">
            <Setter Property="Height" Value="5"/>
        </Style>
        <Style TargetType="RowDefinition" x:Key="HeightPad">
            <Setter Property="Height" Value="100"/>
        </Style>
       .....
       .....
    </Page.Resources>

The Grid generation was then changed to be created programmatically with Buttons added programmatically as well. In the MainPage XAML the only specification for the Grid is its name. Everything else is in code behind so the Grid dimensions become parameters that can be ultimately be sent from the remote app.

        public void InitTheGrid(int x, int y, int Height,  int Width, int space)
        {
            TheGrid.Children.Clear();
            for (int i = 0; i<x;i++)
            {
                RowDefinition rd1 = new RowDefinition();
                rd1.Height = new GridLength((double)space);
                TheGrid.RowDefinitions.Add(rd1);
                RowDefinition rd2 = new RowDefinition();
                rd2.Height = new GridLength((double)Height);
                TheGrid.RowDefinitions.Add(rd2);
            }
            for (int j = 0; j < y; j++)
            {
                ColumnDefinition cd1 = new ColumnDefinition();
                cd1.Width = new GridLength((double)space);
                TheGrid.ColumnDefinitions.Add(cd1);
                ColumnDefinition cd2 = new ColumnDefinition();
                cd2.Width = new GridLength((double)Width);
                TheGrid.ColumnDefinitions.Add(cd2);
            }
        }

This generates a spacing row and button row for each x, and a spacing column and button column for each y. The spacing can though be specified as a grid properties, .RowSpacing and .ColunSpacing. Also with .NET 4 onwards, we can have optional parameters in a function with default values for the options. Note that the options have to be at the end of the parameter list:

        public void InitTheGrid(int x, int y, 
int Height = DefaultCellHeight, int Width = DefaultCellWidth, int space = DefaultCellSpacing) { TheGrid.Children.Clear(); TheGrid.RowSpacing = space; TheGrid.ColumnSpacing = space; for (int i = 0; i<x;i++) { RowDefinition rd2 = new RowDefinition(); rd2.Height = new GridLength((double)Height); TheGrid.RowDefinitions.Add(rd2); } for (int j = 0; j < y; j++) { ColumnDefinition cd2 = new ColumnDefinition(); cd2.Width = new GridLength((double)Width); TheGrid.ColumnDefinitions.Add(cd2); } }

The default values are defined as constants:

        const int DefaultCellWidth= 120;
        const int DefaultCellHeight = 120;
        const int DefaultCellSpacing = 10

 

The Button UserControl

As the number of buttons is not know at design time, the button needs to be defined in a UserControl. Instances of the UserControl is then added to the Grid with in the Grid generation loop above, or a similar loop.

    <Grid>
        <Border x:Name="Borderx" BorderThickness="1" BorderBrush="Black" Background="LightGreen" 
                Padding="0"
                CornerRadius="5”>
            <Button x:Name="TheText"  
                       VerticalAlignment="Center" HorizontalAlignment="Center" 
                        Background="Transparent"
                        Tapped="TheText_Tapped" >
            </Button>
        </Border >
    </Grid>

The Button’s UserControl XAML code

The option was taken to insert the TextBlock programmatically within the buttonn rather than in the UserCotrol’s XAML code. The UserControl has a writeonly Text property that when set, inserts the TextBlock within the Button::

        //Enable wrapped text on button
        public string Text {
            set { TheText.Content =
                    new TextBlock
                    {
                        Text = value,
                        TextWrapping = TextWrapping.Wrap,
                        TextAlignment = TextAlignment.Center,
                    };
            }
         }

The Button has a number of configurable properties as indicated by its constructor parameters:

        public MyUserControl1(int row, int col, string text, Grid containerGrid, 
            //Optional parameters:
            //Name or Id should be unique
            string name="", Brush background = null, 
            int id = FlagForDefaultVal, int cnrRad= FlagForDefaultVal,
            int colSpan= FlagForDefaultVal, int rowSpan = FlagForDefaultVal
            )
        {

The Grid is passed to the constructor so the Button instance can be added to the grid within the constructor. Optional parameters are again used. the constant FlagForDefaultVal is –1. When this value is received the default value for that property is used. Whilst for scalar values this is trivial, for object values its a way of flagging the need use a default object value. (Defaults in optional parameters can’t be objects, except null, as far as I can see). eg:

            if (cnrRad == FlagForDefaultVal)
                Borderx.CornerRadius = DefaultCornerRadius;
            else
                Borderx.CornerRadius = new CornerRadius(cnrRad);

Nb:

        static readonly  Brush DefaultBackground =  new SolidColorBrush (Colors.Beige);
        const int FlagForDefaultVal = -1;


const int CornerRadiusVal = 5; public CornerRadius DefaultCornerRadius { get { return new CornerRadius(CornerRadiusVal); } }

Also the default RowSpan and default ColumnSpan are both 1.

 

Adding Buttons to the Grid

This implemented as a function in the MainPage codebehind:

        private void AddMyUserControl1(int row, int col, string text, 
            //Optional parameters:
            //Name or Id should be unique
            string name = "", Brush background = null,
            int id = -1, int cnrRad = -1, int colSpan=1, int rowSpan=1)
        {
            buttons[row][col] = new uc.MyUserControl1(row,col,text,TheGrid,name,background,id,cnrRad, colSpan, rowSpan);
        }

With some calls from within the MainPage constructor:

            Brush red = new SolidColorBrush(Colors.Red);
            AddMyUserControl1( 0, 0,"arc1", "First", red,123,50,1,2);
            AddMyUserControl1(1, 1, "arc2", "Second", null, 124, 5, 2);
            AddMyUserControl1(2, 2, "The quick brown fox jumps over the lazy dog",  "Third");

To make this all work there needs some further code added to the Grid generation loop. The Buttons are collected into an array as generated so the array needs to be created (as in blue):

        private uc.MyUserControl1 buttons = new uc.MyUserControl1[0];


public void InitTheGrid(int x, int y, int Height = DefaultCellHeight, int Width = DefaultCellWidth, int space = DefaultCellSpacing) { TheGrid.Children.Clear(); TheGrid.RowSpacing = space; TheGrid.ColumnSpacing = space; buttons = new uc.MyUserControl1[x]; for (int i = 0; i<x;i++) { buttons[i] = new uc.MyUserControl1[y]; RowDefinition rd2 = new RowDefinition(); rd2.Height = new GridLength((double)Height); TheGrid.RowDefinitions.Add(rd2); } for (int j = 0; j < y; j++) { ColumnDefinition cd2 = new ColumnDefinition(); cd2.Width = new GridLength((double)Width); TheGrid.ColumnDefinitions.Add(cd2); } }

Events

One last thing to do with this version of the app, pass the tapped event up to the main app from the control.

The code to do this within teh control is:

        //Send back button Name and Id
        public static event TypedEventHandler<string,int> ButtonTapped;
        private void TheText_Tapped(object sender, TappedRoutedEventArgs e)
        {
            if (ButtonTapped != null)
            {
                ButtonTapped(this.Name, this.Id);
            }
        }

The event is specified as static so that only one event handler is required in the main app. The main then can then pass back the name or id back to the remote app. The TypedEventHandler is used as button’s name and/or id is used to identify buttons.

 

Conclusion

This article demonstrates how to create RoundedBox buttons in XAML with multiline text content. It also demonstrates creating a grid whose dimensions aren’t know at design time, to place instances of the buttons within at runtime (using code behind).


Next: Specifying the buttons and grid parameters in Json