As I promised I would the other day, I went ahead and implemented my stupid little slideshow app in both C# and Clojure. It was an interesting exercise, the more so because I’m not entirely sure what conclusions to draw. I may have more to say about this at some point in the future, but for now I thought I’d just put up the code and share some general observations. So, you can find the C# version here, and the Clojure version here.
The app is pretty simple. It starts a low-priority thread that lazily populates a list of filenames that are descendants of a particular directory. Meanwhile, a timer procedure randomly selects one of the filenames out of the ones that have been found so far and invalidates the screen. The repaint simply draws the image centered on a fullscreen window with a black background. If no images have been found yet, then it prints “Working…” on the screen instead.
The Clojure version isn’t really an app, because I wrote all this code really quickly and didn’t bother giving it a main() function, but other than that they’re about as similar as I could make them, given that one is running against Swing on the JVM and the other against Windows Forms on the CLR. I suppose I could have written the Clojure one against Windows Forms as well using ClojureCLR, but honestly it was simply more interesting to me to use Swing, as I’m not familiar with it. And this is anything but a scientific experiment.
For the C# version, I eschewed the Forms designer to keep it closer to the spirit of the Clojure one. And for something like this, where I’m just blasting raw pixels at the screen, I think that was a reasonable choice.
I wrote the Clojure version first. Perhaps that was a bad choice, since it meant I spent a lot more time on the Clojure one, since I had to figure out all the little details, like how to center an image in a region. It certainly made the C# version a lot easier to write, since I was pretty much just translating. But the C# version probably would have been easier for to write in any event, since I’ve been writing C# professionally for almost 10 years, and Clojure in my spare time for less than two.
One thing that surprised me a bit is that the Clojure version turned out to be not much shorter than the C# version. Indeed, if I had put closing braces on the same line as the statement they close (which is like the Clojure style I use), I’d bet they’re about the same. I had expected it to be quite a bit more compact, since, as I’ve said before, my experience with Clojure has been that it generally lets you be more concise. In this case, I suspect it has to do with the fact that I had a lot of Swing-related code, which doesn’t lend itself strongly to the sorts of concision that Clojure supports. Or it could be the fact that I’m a Clojure n00b, and just don’t know how to structure the code to hit a good balance of terseness with readability. Certainly, I could make it a bit shorter by inlining calls to things like make-frame. Other recommendations as to how better to write either version would be more than welcome.
Both versions of the code are pretty rough, as I wrote them essentially while I was waiting for stuff to install on a VM. I’d definitely clean them up more if I were going to do anything with them, but I figured it was better to get it up here than to wait potentially forever to get it polished. Really, I’m just trying to point out that not all my code looks this crappy. :)
You’ll note that in the Clojure version, I use an atom to store the list of filenames. That’s not because I’m taking advantage of Clojure’s awesome concurrency support, it’s because it was a convenient thing to do. Note that the C# version doesn’t bother with synchronization at all, since I’ve only got two threads, and one is only reading, and one is only appending. If I wanted to get fancier with the threads (e.g. have another thread doing look-ahead loading of the next image), then I might have to start doing synchronization. I’d be in good shape on the Clojure side then, since not much would have to change, and the fact that Clojure values are immutable really helps. I don’t think the C# version wouldn’t need too much work, but I’d have to think about it more to be sure, and it would depend on exactly what I was trying to do.
It’s apropos of nothing, but it made me smile that the C# version required PInvoke to get a fullscreen window, and the Swing one just supported it out-of-the-box.
Being familiar with Visual Studio was a definite benefit when working with the C# code. I’m not very facile with the debugging capabilities of Emacs/SLIME, which is what I use to develop Clojure, and when I had problems it took me a little longer to figure them out. That said, having the REPL was really awesome, and it really makes me hate the Immediate Window in Visual Studio by comparison. It would be interesting to try some of the other Clojure IDEs to see how they stack up.
Neither version of the code has much in the way of comments. Sorry about that. :)
Anyway, that’s my pile of unstructured thoughts. Hope it’s of interest, if not value, to at least some of you. And as I said, further discussion, rewrites, comments, and criticism are wholly welcome.
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using System.Windows.Forms; | |
using System.Runtime.InteropServices; | |
using System.Drawing; | |
using System.Threading; | |
using Timer = System.Windows.Forms.Timer; | |
using System.IO; | |
namespace Slideshow | |
{ | |
public class MainWindow : Form | |
{ | |
// Interop code from http://www.codeproject.com/KB/cs/FullScreenDotNetApp.aspx | |
[DllImport("user32.dll", EntryPoint = "GetSystemMetrics")] | |
private static extern int GetSystemMetrics(int which); | |
[DllImport("user32.dll")] | |
private static extern void SetWindowPos(IntPtr hwnd, IntPtr hwndInsertAfter, | |
int x, int y, int width, int height, uint flags); | |
private const int SM_CXSCREEN = 0; | |
private const int SM_CYSCREEN = 1; | |
private static IntPtr HWND_TOP = IntPtr.Zero; | |
private const int SWP_SHOWWINDOW = 0x0040; | |
private readonly Random _rng = new Random(); | |
private readonly List<string> _imageList = new List<string>(); | |
private Image _currentImage; | |
private string _dir; | |
public MainWindow(string dir) | |
{ | |
_dir = dir; | |
StartImageListPopulation(); | |
MakeTimer(); | |
WindowState = FormWindowState.Maximized; | |
FormBorderStyle = FormBorderStyle.None; | |
TopMost = true; | |
SetWindowPos(Handle, HWND_TOP, 0, 0, ScreenWidth, ScreenHeight, SWP_SHOWWINDOW); | |
BackColor = Color.Black; | |
} | |
private int ScreenWidth | |
{ | |
get { return GetSystemMetrics(SM_CXSCREEN); } | |
} | |
private int ScreenHeight | |
{ | |
get { return GetSystemMetrics(SM_CYSCREEN); } | |
} | |
private void MakeTimer() | |
{ | |
var timer = new Timer(); | |
timer.Interval = 5000; | |
timer.Tick += new EventHandler(timer_Tick); | |
timer.Enabled = true; | |
} | |
private static IEnumerable<string> GetDirectoryDescendants(string path) | |
{ | |
return Directory.GetFiles(path) | |
.Concat(Directory.GetDirectories(path) | |
.SelectMany(directory => GetDirectoryDescendants(directory))); | |
} | |
private static bool IsJpeg(string file) | |
{ | |
return Path.GetExtension(file).Equals(".jpg", StringComparison.InvariantCultureIgnoreCase); | |
} | |
private void PopulateImageList() | |
{ | |
var descendants = GetDirectoryDescendants(_dir).Where(f => IsJpeg(f)); | |
foreach (var file in descendants) | |
{ | |
_imageList.Add(file); | |
} | |
} | |
private void StartImageListPopulation() | |
{ | |
var thread = new Thread(PopulateImageList); | |
thread.Priority = ThreadPriority.Lowest; | |
thread.Start(); | |
} | |
private string RandomImagePath() | |
{ | |
var n = _imageList.Count; | |
if (n == 0) | |
{ | |
return null; | |
} | |
else | |
{ | |
return _imageList[_rng.Next(n)]; | |
} | |
} | |
private Image RandomImage() | |
{ | |
var path = RandomImagePath(); | |
if (path == null) | |
{ | |
return null; | |
} | |
return Image.FromFile(path); | |
} | |
private void timer_Tick(object sender, EventArgs e) | |
{ | |
_currentImage = RandomImage(); | |
Invalidate(); | |
} | |
private Rectangle FitTo(Size imageDims, Size regionDims) | |
{ | |
var scaling = Math.Min(1.0F, Math.Min((float) regionDims.Width / (float) imageDims.Width, (float) regionDims.Height / (float) imageDims.Height)); | |
var scaledImageDims = Scale(scaling, imageDims); | |
var offset = Center(scaledImageDims, regionDims); | |
return new Rectangle(offset, scaledImageDims); | |
} | |
private Point Center(Size imageDims, Size regionDims) | |
{ | |
Func<int, int, int> average = (i, r) => (r / 2) - (i / 2); | |
return new Point(average(imageDims.Width, regionDims.Width), average(imageDims.Height, regionDims.Height)); | |
} | |
private Size Scale(float factor, Size dims) | |
{ | |
return new Size((int) (dims.Width * factor), (int) (dims.Height * factor)); | |
} | |
protected override void OnPaint(PaintEventArgs e) | |
{ | |
var g = e.Graphics; | |
g.Clear(Color.Black); | |
if (_currentImage == null) | |
{ | |
g.DrawString("Working...", Font, Brushes.White, 800, 600); | |
} | |
else | |
{ | |
var regionDims = new Size(ScreenWidth, ScreenHeight); | |
var imageDims = new Size(_currentImage.Width, _currentImage.Height); | |
var destination = FitTo(imageDims, regionDims); | |
g.DrawImage(_currentImage, destination); | |
} | |
} | |
} | |
public class Program | |
{ | |
static void Main(string[] args) | |
{ | |
Application.Run(new MainWindow(args[0])); | |
} | |
} | |
} |