Kean Walmsley


  • About the Author
    Kean on Google+

July 2014

Sun Mon Tue Wed Thu Fri Sat
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31    








« Countdown to DevCamps 2010 | Main | Hitting breakpoints in .NET Class Libraries while debugging with Visual Studio 2010 »

April 23, 2010

Importing Photosynth point clouds into AutoCAD 2011 - Part 3

As alluded to in the last post in this series (ignoring a related post that dealt with user interface integration) I wasn’t really happy with some of the tricks I needed in the WinForms version to try and make a coherent user interface for tracking accessed point clouds in a hosted Photosynth browsing session. This post replaces the WinForms UI with one implemented using WPF, and in fact might also have been titled “Using data-binding in WPF to track a list of objects with associated thumbnails” or something to that effect. :-)

What I’ve done in the new version of the solution used to build this tool is to add a separate WPF application project (called BrowsePhotosynth) while leaving the WinForms project there as a historical reference (that one is still called Browser). The new WPF project builds an executable called “ADNPlugins-BrowsePhotosynth2.exe”, which has made it easy to update the DLL project to work with this new version – all we have to do is add a “2” in a few places. Otherwise the code in the main DLL module remains unchanged (and won’t be listed in this post). Please refer back to the previous post for instructions on getting the application to work: if you have already got the last version working, you should be able to drop the new DLL and EXE into the same folder and they should just work.

Let’s now take a look at the pertinent files from the project for our new-fangled WPF browser. Bear in mind that I’m far from being a WPF expert, myself (I do feel as though I’m making progress, though), so I’ve relied a lot on code I’ve found on the web for various tasks: accessing command-line arguments, automatically updating the UI when bound properties change, adding a splitter, enabling selection on hover, using a gradient selection style and changing a button’s colour when being hovered over. Which means the code may have redundant properties set or use inconsistent approaches to solving the various design problems, but anyway. We’re all learning. :-)

Firstly we have the main App.xaml file, which only has one interesting change, and that’s to call our Application_Startup() event before the UI is shown:

<Application x:Class="BrowsePhotosynth.App"

  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

  StartupUri="BrowsePhotosynth.xaml"

  Startup="Application_Startup">

  <Application.Resources>       

  </Application.Resources>

</Application>

And in the associated App.xaml.cs “code-behind” we have that function’s implementation:

using System.Windows;

 

namespace BrowsePhotosynth

{

  public partial class App : Application

  {

    internal static int _hwnd;

 

    private void Application_Startup(

      object sender, StartupEventArgs e

    )

    {

      // Extract the handle passed as an argument.

      // This is AutoCAD's main window, and we'll use it

      // to pump messages. If there's no handle, set it

      // to 0, which means "standalone mode"

 

      _hwnd =

        (e.Args.Length > 0 ? int.Parse(e.Args[0]) : 0);

    }

  }

}

The only notable difference to the equivalent code from the WinForms version is that we need to look at item 0 in the argument array rather than item 1. Otherwise we store the HWnd as a static integer which we can then access from our main browser implementation.

Here’s the BrowsePhotosynth.xaml file (the indentation is a little off, in terms of where attributes are relative to their elements, but anyway):

<Window

  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

  xmlns:wfi=

  "clr-namespace:System.Windows.Forms.Integration;assembly=WindowsFormsIntegration"

  xmlns:wb="clr-namespace:csExWB;assembly=csExWB"

  x:Class="BrowsePhotosynth.PhotosynthBrowser"

  Loaded="WindowLoaded"

  Closing="WindowClosing"

  Title="Browse Photosynth"

  Height="600"

  Width="800"

  Topmost="True"

  WindowStartupLocation="CenterScreen"

  Icon="/ADNPlugin-BrowsePhotosynth2;component/Browser.ico">

  <Window.Resources>

    <!--

      Style to make our list box have that nice gradient

      selection style, but also to hook the slider up to

      adjust the size of the list items

    -->

    <Style

    x:Key="FocusListBoxItemStyle"

    TargetType="{x:Type ListBoxItem}">

      <!--

        Hook up our slider to both width and height properties

      -->

      <Setter

      Property="Width"

      Value=

        "{Binding Path=Value, ElementName=sizeSlider, Mode=TwoWay}"

      />

      <Setter

      Property="Height"

      Value=

        "{Binding Path=Value, ElementName=sizeSlider, Mode=TwoWay}"

      />

      <!--

        Bind the item alignment to the container's

      -->

      <Setter

      Property="HorizontalContentAlignment"

      Value=

        "{Binding

            Path=HorizontalContentAlignment,

            RelativeSource=

              {RelativeSource

                AncestorType= {x:Type ItemsControl}}}"

      />

      <Setter

      Property="VerticalContentAlignment"

      Value=

        "{Binding

            Path=VerticalContentAlignment,

            RelativeSource=

              {RelativeSource

                AncestorType={x:Type ItemsControl}}}"

      />

      <!--

        Here's the border we add when highlighting

      -->

      <Setter Property="Template">

        <Setter.Value>

          <ControlTemplate TargetType="{x:Type ListBoxItem}">

            <Border

            Name="listItemFocusBorder"

            SnapsToDevicePixels="true"

            Background="{TemplateBinding Background}"

            BorderBrush="{TemplateBinding BorderBrush}"

            BorderThickness="{TemplateBinding BorderThickness}"

            Padding="{TemplateBinding Padding}">

              <ContentPresenter

              HorizontalAlignment=

                "{TemplateBinding HorizontalContentAlignment}"

              VerticalAlignment=

                "{TemplateBinding VerticalContentAlignment}"

              SnapsToDevicePixels=

                "{TemplateBinding SnapsToDevicePixels}"

              />

            </Border>

            <!--

              We only add it when an list item is selected

            -->

            <ControlTemplate.Triggers>

              <Trigger Property="IsSelected" Value="true">

                <Setter

                Property="Foreground"

                Value=

                  "{DynamicResource

                    {x:Static SystemColors.HighlightTextBrushKey}}"

                />

                <!--

                  Our gradient backrgound style

                -->

                <Setter

                Property="Background"

                TargetName="listItemFocusBorder">

                  <Setter.Value>

                    <!--

                      Gradient from Photosynth green to black

                    -->

                    <LinearGradientBrush

                    EndPoint="0.5,1"

                    StartPoint="0.5,0">

                      <GradientStop Color="#B4E800" Offset="0"/>

                      <GradientStop Color="Black" Offset="1"/>

                    </LinearGradientBrush>

                  </Setter.Value>

                </Setter>

              </Trigger>

            </ControlTemplate.Triggers>

          </ControlTemplate>

        </Setter.Value>

      </Setter>

    </Style>

    <!--

      Style to make our button get a different look (with

      a nice green background, but also a bolder, black

      font) when it is hovered over

    -->

    <Style x:Key="FocusButtonStyle" TargetType="{x:Type Button}">

      <Setter Property="Template">

        <Setter.Value>

          <ControlTemplate TargetType="{x:Type Button}">

            <!--

              Create our border for manipulation

            -->

            <Border

            Name="buttonFocusBorder"

            CornerRadius="5"

            BorderThickness="3"

            BorderBrush="White"

            Background="Black">

              <ContentPresenter

              HorizontalAlignment="Center"

              VerticalAlignment="Center"

              />

            </Border>

            <ControlTemplate.Triggers>

              <!--

                When the button is hovered overm change the

                background to Photosynth green, the forground

                to black, and the font-weight to bold

              -->

              <Trigger

              Property="IsMouseOver"

              Value="True">

                <Setter

                TargetName="buttonFocusBorder"

                Property="Background"

                Value="#B4E800"

                />

                <Setter

                TargetName="buttonFocusBorder"

                Property="Button.Foreground"

                Value="Black"

                />

                <Setter

                TargetName="buttonFocusBorder"

                Property="Button.FontWeight"

                Value="Bold"

                />

              </Trigger>

            </ControlTemplate.Triggers>

          </ControlTemplate>

        </Setter.Value>

      </Setter>

    </Style>

  </Window.Resources>

  <!--

    Now for our actual window elements

  -->

  <Grid

  Name="grid"

  Background="Black">

    <!--

      Start with a three column outer grid for our browser,

      our splitter and our point cloud list-related stuff

    -->

    <Grid.ColumnDefinitions>

      <ColumnDefinition Width="450*" MinWidth="50" />

      <ColumnDefinition Width="10*" MinWidth="3" />

      <ColumnDefinition Width="170*" MinWidth="30" />

    </Grid.ColumnDefinitions>

    <!--

      Add our browser to the left, hosted by a WinForms

      host

      Be warned - the browser control causes VS 2008 to

      act very flakily. Expect regular crashes and restarts

    -->

    <wfi:WindowsFormsHost Grid.Column="0" Name="wfh">

      <wb:cEXWB

      x:Name="browser"

      ProtocolHandlerBeginTransaction="HttpTransaction"

      />

    </wfi:WindowsFormsHost>

    <!--

      Now our splitter control in the middle

    -->

    <GridSplitter

    ResizeDirection="Columns"

    Grid.Column="1"

    Width="3"

    HorizontalAlignment="Center"

    />

    <!--

      And our nested grid containing three rows for the

      clear button, the list itself and the slider

    -->

    <Grid

    Grid.Column="2"

    Background="Black">

      <Grid.RowDefinitions>

        <RowDefinition Height="32"/>

        <RowDefinition Height="*"/>

        <RowDefinition Height="30"/>

      </Grid.RowDefinitions>

      <!--

        Add the button to clear the list

      -->

      <Button

      Name="clearButton"

      Grid.Row="0"

      Height="25"

      Background="Black"

      Foreground="White"

      Style="{StaticResource FocusButtonStyle}"

      Click="ClearButton_Click">

        Clear Point Cloud History

      </Button>

      <!--

        Now the list of accessed point clouds

      -->

      <ListView

      Name="clouds"

      Grid.Row="1"

      Background="Black"

      MinHeight="200"

      BorderThickness="0"

      ItemContainerStyle=

        "{DynamicResource FocusListBoxItemStyle}">

        <ListBox.ItemsPanel>

          <ItemsPanelTemplate>

            <StackPanel IsItemsHost="True"/>

          </ItemsPanelTemplate>

        </ListBox.ItemsPanel>

        <ListBox.ItemTemplate>

          <DataTemplate>

            <Viewbox

            MouseDown="Viewbox_MouseDown"

            MouseEnter="ListBoxItem_MouseEnter">

              <Border

              Padding="20,20,10,10"

              Margin="5">

                <StackPanel>

                  <!--

                    Add the image with the default size

                  -->

                  <Image

                  Width="170"

                  Opacity="20"

                  OpacityMask="Black"

                  HorizontalAlignment="Stretch"

                  VerticalAlignment="Stretch"

                  Height="170"

                  Source="{Binding Path=ImagePath}"

                  />

                  <!--

                    Add the name of the point cloud in green

                  -->

                  <TextBlock

                  Foreground="#B4E800"

                  Text="{Binding Path=Name}"

                  TextAlignment="Center"

                  TextWrapping="Wrap"

                  Width="170"

                  />

                </StackPanel>

              </Border>

            </Viewbox>

          </DataTemplate>

        </ListBox.ItemTemplate>

      </ListView>

      <!--

        And lastly the slider for the list item size

      -->

      <Slider

      Name="sizeSlider"

      Grid.Row="2"

      Width="200"

      Orientation="Horizontal"

      VerticalAlignment="Center"

      Value="170"

      IsSnapToTickEnabled="True"

      Minimum="50"

      Maximum="400"

      TickPlacement="BottomRight"

      TickFrequency="10"

      />

    </Grid>

  </Grid>

</Window>

The comments should go some way to explaining the logic behind the design.

The “code-behind” file is actually in many ways less complicated than its predecessor, as we aren’t having to deal with custom draw code (that’s taken care of in the XAML, now).

Here’s a summary of the main enhancements/optimizations:

  • We now use data-binding to display the information we have about our synths
    • In the previous version we had to maintain various lists (the synth data, the images and the visible list), so this has simplified things greatly
    • We had a little more plumbing to do to make sure our properties implement INotifyPropertyChanged properly, so that the UI gets updated automatically when the properties change
    • We also have to use an ObservableCollection<> rather than a List<> for our SynthInfo objects, which allow the notifications to propagate properly. Luckily this doesn’t result in a change in the XML data stored for our synths, which is cool
    • WPF had issues with dependency properties such as this being updated from other threads, so we no longer use a timer to update our images
      • Dispatcher.Invoke() is actually a better way for queuing up this task, in any case, and makes life much simpler
  • Our Window is constructed via App.xaml, and so can no longer take arguments
    • We now set the URL in our form constructor and get AutoCAD’s HWnd from the app object, where it gets set as a static variable on startup
  • Our events are now hooked up via XAML, which streamlines our form’s constructor
  • The csExWb control causes an annoying COM exception in a WPF application on shutdown, which is probably a clue to the issue we were seeing with the WinForms application (and originally with AutoCAD, when I had it hosted in a NETLOADed DLL)
    • Because the exception is more visible from WPF, we don’t ever exit the application, even when the “X” is used by the user
    • We now just hide the form and let AutoCAD kill the process when the BP command is next called
  • This version of the application doesn’t have a right-click menu for importing the point clouds
    • This is should be a trivial thing to add back, in case
  • In terms of the UI changes, we have a nice little slider that allows the user to change the size of the images in the list, which is quite handy
    • It allows you to zoom into the image to help decide whether to import it

Right, that’s about it for the changes. Here’s the updated C# code file, BrowsePhotosynth.xaml.cs:

using System;

using System.Collections.Generic;

using System.Collections.ObjectModel;

using System.ComponentModel;

using System.IO;

using System.Net;

using System.Runtime.InteropServices;

using System.Text;

using System.Text.RegularExpressions;

using System.Threading;

using System.Windows.Threading;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Input;

using System.Xml;

using System.Xml.Serialization;

 

namespace BrowsePhotosynth

{

  /// <summary>

  /// Interaction logic for BrowsePhotosynth.xaml

  /// </summary>

  public partial class PhotosynthBrowser : Window

  {

    // A Win32 function we'll use to send messages to AutoCAD

 

    [DllImport("user32.dll")]

    private static extern IntPtr SendMessageW(

      IntPtr hWnd, int Msg, IntPtr wParam,

      ref COPYDATASTRUCT lParam

    );

 

    // And the structure we'll require to do so

 

    private struct COPYDATASTRUCT

    {

      public IntPtr dwData;

      public int cbData;

      public IntPtr lpData;

    }

 

    public abstract class NotifyPropertyChangedBase :

      INotifyPropertyChanged

    {

      #region INotifyPropertyChanged Members

      public event PropertyChangedEventHandler PropertyChanged;

      #endregion

 

      #region Methods

      protected bool CheckPropertyChanged<T>(

        string propertyName, ref T oldValue, ref T newValue

      )

      {

        if (oldValue == null && newValue == null)

          return false;

 

        if ((oldValue == null && newValue != null) ||

          !oldValue.Equals((T)newValue))

        {

          oldValue = newValue;

          FirePropertyChanged(propertyName);

          return true;

        }

        return false;

      }

 

      protected void FirePropertyChanged(string propertyName)

      {

        if (PropertyChanged != null)

        {

          PropertyChanged(

            this, new PropertyChangedEventArgs(propertyName)

          );

        }

      }

 

      #endregion

    }

 

    // A class containing the browsing information about each

    // point-cloud. This is made serializable to XML, to

    // allow easy persistence

 

    [XmlRoot("Synth")]

    public class SynthInfo : NotifyPropertyChangedBase

    {

      // The name of our Photosynth

 

      private string _name;

 

      [XmlAttribute("Name")]

      public string Name

      {

        get { return _name; }

        set

        {

          if (CheckPropertyChanged<string>(

                "Name", ref _name, ref value))

            FirePropertyChanged("Name");

        }

      }

 

      // Its URL

 

      private string _url;

 

      [XmlElement("Url")]

      public string Url

      {

        get { return _url; }

        set

        {

          if (CheckPropertyChanged<string>(

                "Url", ref _url, ref value))

            FirePropertyChanged("Url");

        }

      }

 

      // The location of its image file

 

      private string _image;

 

      [XmlElement("Image")]

      public string Image

      {

        get { return _image; }

        set

        {

          if (CheckPropertyChanged<string>(

                "Image", ref _image, ref value))

            FirePropertyChanged("ImagePath");

        }

      }

 

      // A more complete location for the image

      // (not persisted to XML)

 

      public string ImagePath

      {

        get { return _imagePath + _image; }

      }

    }

 

    // The location of our JPGs, PCGs and the history XML

 

    private static string _imagePath = "";

 

    // We store a central list of these SynthInfo objects

 

    private ObservableCollection<SynthInfo> _synths =

      new ObservableCollection<SynthInfo>();

 

    // Public property for the URL loaded into the browser

    // (this is the URL of the browser, which is typically

    // "http://photosynth.net")

 

    private string _url = null;

    public string Url

    {

      set { _url = value; }

      get { return _url; }

    }

 

    // Public property for the handle of the AutoCAD application

    // we're connected to

 

    private int _hwnd = 0;

    public int HWnd

    {

      set { _hwnd = value; }

      get { return _hwnd; }

    }

 

    // Internal constants

 

    const string pointsName = "points_0_0.bin";

    const string suffix = " - Photosynth";

    const string historyXml = "BrowsingHistory.xml";

    const int imgWidth = 128;

    const int imgHeight = imgWidth;

 

    // Form constructor

 

    public PhotosynthBrowser()

    {

      InitializeComponent();

 

      _imagePath =

        Environment.GetFolderPath(

          Environment.SpecialFolder.MyDocuments

        ) + "\\Photosynth Point Clouds\\";

 

      _url = "http://photosynth.net";

      _hwnd = App._hwnd;

    }

 

    private void WindowClosing(

      object sender, System.ComponentModel.CancelEventArgs e

    )

    {

      // Store our browsing history to XML

 

      SerializeHistory(_synths);

 

      // If running from AutoCAD, just hide the application,

      // as we need to kill it rather then let it exit

      // (the browser control causes a COM Exception in WPF)

 

      if (_hwnd > 0)

      {

        e.Cancel = true;

        Hide();

      }

    }

 

    private void WindowLoaded(object sender, RoutedEventArgs e)

    {

      // Navigate to the provided URL, if it's non-null and

      // not the one we're already pointed at

 

      if (!String.IsNullOrEmpty(_url) &&

          browser.LocationUrl.ToString() != _url)

        browser.Navigate(_url);

 

      // Load our browsing history from the XML file.

 

      _synths = DeserializeHistory();

      clouds.ItemsSource = _synths;

      UpdateImages();

    }

 

    // Select an item when the mouse hovers over it

 

    private void ListBoxItem_MouseEnter(

      object sender, MouseEventArgs e

    )

    {

      clouds.SelectedItem = (sender as Viewbox).DataContext;

      if (!clouds.IsFocused)

        clouds.Focus();

    }

 

    // When an item in our list is clicked on

 

    private void Viewbox_MouseDown(

      object sender, MouseButtonEventArgs e

    )

    {

      if (clouds.SelectedItems.Count == 1)

      {

        if (e.LeftButton == MouseButtonState.Pressed)

          ImportPointCloud();

      }

    }

 

    // Clear our history list when the clear button is clicked

 

    private void ClearButton_Click(object sender, RoutedEventArgs e)

    {

      _synths.Clear();

 

      string histFile = _imagePath + historyXml;

      if (File.Exists(histFile))

        File.Delete(histFile);

    }

 

    // Find out when HTTP traffic is in progress

 

    private void HttpTransaction(

      object sender,

      csExWB.ProtocolHandlerBeginTransactionEventArgs e

    )

    {

      // If we detect the first point cloud file in a series...

 

      if (e.URL.Contains(pointsName))

      {

        csExWB.cEXWB wb = (csExWB.cEXWB)sender;

 

        // Get the page's title and extract the URL

 

        string title = wb.GetTitle(true);

 

        // If the point cloud was embedded in the main page,

        // let's extract its actual title from the HTML content

        // (this will change as the Photosynth page structure

        // changes, but if it doesn't find the relevant entries

        // then we just use the overall title)

 

        if (title.StartsWith("Photosynth"))

        {

          string src = wb.DocumentSource;

          if (src.Contains("title-block"))

          {

            src = src.Substring(src.IndexOf("title-block"));

            if (src.Contains("A href="))

            {

              src = src.Substring(src.IndexOf("A href="));

              if (src.Contains(">"))

              {

                src = src.Substring(src.IndexOf(">"));

                if (src.Contains("<"))

                {

                  int endPos = src.IndexOf("<");

                  if (endPos > 1)

                  {

                    title = src.Substring(1, endPos - 1);

                  }

                }

              }

            }

          }

        }

        else if (title.EndsWith(suffix))

        {

          // Strip off the common suffix, if it's there

 

          title =

            title.Substring(0, title.Length - suffix.Length);

        }

 

        // Extract the base URL, without the initial point-cloud

        // name

 

        string baseUrl =

          e.URL.Substring(0, e.URL.Length - pointsName.Length);

 

        // Use this info to create a new entry in our list

 

        AddToPointCloudList(title, baseUrl);

 

        // We cannot just start downloading images directly, but

        // we can if we queue up the function call

 

        Dispatcher.Invoke(

          DispatcherPriority.Normal,

          (ThreadStart)delegate() { UpdateImages(); }

        );

      }

    }

 

    // Update the images in our synth list, if needed

 

    private void UpdateImages()

    {

      // Add images for any items which don't yet have them

      // (realistically this will usually be just one image,

      // as we check every second and browsing takes time)

 

      for (int i=0; i < _synths.Count; i++)

      {

        SynthInfo sinf = _synths[i];

 

        if (String.IsNullOrEmpty(sinf.Image))

        {

          string baseUrl = sinf.Url;

 

          // Transform our base URL to get the URL

          // to an appropriate image on the server

 

          if (baseUrl.Contains(".synth_files"))

          {

            string imageUrl =

              baseUrl.Substring(

                0, baseUrl.LastIndexOf(".synth_files")

              )

              + "_files/6/0_0.jpg";

 

            // Create a web client to download the image

 

            WebClient wc = new WebClient();

            using (wc)

            {

              string locFile =

                MakeValidFileName(sinf.Name) + ".jpg";

              string locImage = _imagePath + locFile;

 

              // Try to download our image file

 

              try

              {

                wc.DownloadFile(imageUrl, locImage);

              }

              catch

              { }

 

              // If we were successful, load and add it

 

              if (File.Exists(locImage))

              {

                // Make sure our browsing history reflects

                // the existence of the downloaded image

 

                sinf.Image = locFile;

                _synths[i] = sinf;

              }

            }

          }

        }

      }

    }

 

    // Add a point cloud with a certain title and URL to our list

 

    private void AddToPointCloudList(string title, string baseUrl)

    {

      bool found = false;

 

      // First we check that it's not already in the list

 

      foreach (SynthInfo sinf in _synths)

      {

        if (sinf.Name == title && sinf.Url == baseUrl)

        {

          found = true;

          break;

        }

      }

 

      // If it isn't add it to the list and to our browsing history

 

      if (!found)

      {

        SynthInfo sinf = new SynthInfo();

        sinf.Url = baseUrl;

        sinf.Name = title;

        _synths.Add(sinf);

      }

    }

 

    // Import a point cloud into AutoCAD by firing a command

 

    private void ImportPointCloud()

    {

      // If we're not connected to an AutoCAD session (via

      // the handle we received as a command-line argument),

      // then we show a message and continue

 

      if (_hwnd == 0)

      {

        MessageBox.Show(

          "This browser is not connected to an instance of " +

          "AutoCAD. Relaunch from AutoCAD to import Point " +

          "Clouds from your browsing history.",

          "Browse Photosynth",

          MessageBoxButton.OK,

          MessageBoxImage.Information

        );

      }

      else

      {

        // Assume the item in the _synths list is at the same

        // location as the selected item

 

        SynthInfo sinf = _synths[clouds.SelectedIndex];

        string title = sinf.Name;

        string firstUrl = sinf.Url + pointsName;

 

        // Hide the form and stop the browsing operation

 

        Hide();

        browser.NavToBlank();

        browser.Stop();

 

        // Fire off our command to AutoCAD

 

        SendCommandToAutoCAD(

          "_.IMPORTPHOTOSYNTH \"" + firstUrl + "\" \"" +

          title + "\" "

        );

 

        // We no longer exit the application, as a COM exception

        // causes problems with WPF

      }

    }

 

    // Save the current state of our browsing history

    // to an XML file

 

    private void SerializeHistory(

      ObservableCollection<SynthInfo> synths

    )

    {

      if (!Directory.Exists(_imagePath))

        Directory.CreateDirectory(_imagePath);

 

      if (_synths.Count > 0)

      {

        XmlSerializer xs =

          new XmlSerializer(

            typeof(ObservableCollection<SynthInfo>)

          );

        XmlTextWriter xw =

          new XmlTextWriter(_imagePath + historyXml, Encoding.UTF8);

        xs.Serialize(xw, synths);

        xw.Close();

      }

    }

 

    // Read and return the previous browsing history from

    // our stored XML file

 

    private ObservableCollection<SynthInfo> DeserializeHistory()

    {

      string histFile = _imagePath + historyXml;

 

      if (File.Exists(histFile))

      {

        XmlSerializer xs =

          new XmlSerializer(

            typeof(ObservableCollection<SynthInfo>)

          );

        XmlTextReader xr = new XmlTextReader(histFile);

        if (xs.CanDeserialize(xr))

        {

          ObservableCollection<SynthInfo> synths =

            (ObservableCollection<SynthInfo>)xs.Deserialize(xr);

          xr.Close();

          return synths;

        }

      }

      return new ObservableCollection<SynthInfo>();

    }

 

    // Just use the Win32 API to communicate with AutoCAD.

    // We simply need to send a command string, so this

    // approach avoids a dependency on AutoCAD's COM

    // interface

 

    private void SendCommandToAutoCAD(string toSend)

    {

      const int WM_COPYDATA = 0x4A;

 

      COPYDATASTRUCT cds = new COPYDATASTRUCT();

      cds.dwData = new IntPtr(1);

      string data = toSend + "\0";

      cds.cbData = data.Length * Marshal.SystemDefaultCharSize;

      cds.lpData = Marshal.StringToCoTaskMemAuto(data);

 

      SendMessageW(

        new IntPtr(_hwnd),

        WM_COPYDATA,

        new IntPtr(this.HWnd),

        ref cds

      );

 

      Marshal.FreeCoTaskMem(cds.lpData);

    }

 

    // Function to create a valid filename from a string.

    // This has been duplicated from the plugin project

 

    private static string MakeValidFileName(string name)

    {

      string invChars =

        Regex.Escape(new string(Path.GetInvalidFileNameChars()));

      string invRegEx = string.Format(@"[{0}]", invChars + ".");

      return Regex.Replace(name, invRegEx, "-");

    }

  }

}

Let’s see what happens when we run our updated BP command. Right off we should see the same history gets displayed in the new browser:

Browsing Photosynth with WPF 1

You’ll notice black isn’t mapped as the transparent colour in these images, which can be done with WPF but with quite a bit more work than it took with WinForms, unfortunately.

If we hover over the button at the top, we see it highlighted with our green background colour:

Browsing Photosynth with WPF 2

We can use the slider at the bottom to adjust the size of the image to get them all visible in the list:

Browsing Photosynth with WPF 3

And we can slide it the other way to zoom right into the thumbnail images, too:

Browsing Photosynth with WPF 4

Other than that the application should behave as before with respect to AutoCAD.

My next planned enhancement is on the AutoCAD side of things as opposed to the browsing interface. I’d like to to use the Photosynth web service to query information about a particular synth to help optimize the download and processing of the points. Which may mean using F# or perhaps the Task Parallel Library from C# to parallelize much of these operations (taking advantage of multiple cores and the effect of network latency). But that’s for another day. :-)

blog comments powered by Disqus

10 Random Posts