어떤 툴이거나 보통 thumbnail은 원본 파일을 읽어들이고 작은 크기의 thumbnail 이미지를 만든 다음 원본 파일을 thumbnail에 그려넣고 저장하는 순서로 이루어집니다. OSX에서 thumbnail을 만드는 방법은 크게 Cocoa의 NSImage를 사용하는 것과 Quartz의 CoreImage를 사용하는 방법이 있습니다. NSImage가 더 나중의 방법이고 내부적으로는 아마 core image를 사용하는 것 같습니다.
먼저 원본이미지를 읽어들입니다.
img = NSImage.alloc.initWithContentsOfFile("파일이름")
이제 작은 크기의 thumbnail을 만듭니다. 엄청나게 긴 함수이름을 가지고 있습니다.
:pixelsWide=>200.0, :pixelsHigh=>150.0,
:bitsPerSample=>8, :samplesPerPixel=>4, :hasAlpha=>true, :isPlanar=>false,
:colorSpaceName=>NSDeviceRGBColorSpace, :bytesPerRow=>0, :bitsPerPixel=>0)
다음 2행은 graphics context를 thumbnail의 것으로 만들고 원본 이미지를 thumbnail 크기로 그려넣습니다. Graphics context를 thumbnail의 것으로 지정하면 image에서의 그림 그리기가 thumbnail에 이루어집니다.
img.drawInRect([0.0, 0.0, 200.0, 150.0], :fromRect=>NSZeroRect, :operation=>NSCompositeCopy, :fraction=>1.0)
이제 그려진 thumbnail을 파일로 저장합시다.
jpg.writeToFile("썸네일 파일이름", :atomically=>false)
다음은 그 결과물입니다.
그림이 작아지기는 했습니다만 영 거칠고 화질이 좋지 않습니다. 건물벽도 이상하게 보이네요.
이미지의 크기를 확인해 봅시다.
=> #<NSSize width=836.484069824219 height=627.363037109375>
원본은 3648, 2736 크기의 사진입니다. Cocoa에서는 픽셀단위를 사용하지 않고 출력 device, window, view에 따라 다른 단위를 사용합니다. 우리는 비트맵 단위로 다루고 싶으므로 다음과 같은 방법을 써서 크기를 원본으로 맞추어 줍니다.
img.setSize([img_bitmap.pixelsWide, img_bitmap.pixelsHigh])
이후 결과물은 다음과 같습니다.
거의 차이가 없네요. 그림을 그릴때 화질을 조절하는 옵션이 있습니다. 속도는 조금 느리더라도 더 좋은 화질을 얻도록 합시다.
마지막 결과물...
이제야 볼만한 그림이 나온것 같네요.
NSBitmapImageRep 인스턴스의 메소드 representationUsingType은 JPEG이외에 TIFF, BMP, GIF, PNG 그리고 JPEG2000까지 지원해줍니다. 그리고 properties 인자에 이미지의 옵션을 지정해 줄 수 있습니다. 예를 들어 JPEG의 화질을 95%로 하고 싶다면 인자로 { NSImageCompressionFactor => 0.95 }를 주면 됩니다.
다음은 전체 소스입니다.
NSGraphicsContext.saveGraphicsState
img = NSImage.alloc.initWithContentsOfFile("test.jpg")
img_bitmap = img.bestRepresentationForDevice(nil)
img.setSize([img_bitmap.pixelsWide, img_bitmap.pixelsHigh]);
thumbrect = [0.0, 0.0, 200.0, 150.0]
thumb = NSBitmapImageRep.alloc.initWithBitmapDataPlanes(nil,
:pixelsWide=>thumbrect[2], :pixelsHigh=>thumbrect[3],
:bitsPerSample=>8, :samplesPerPixel=>4, :hasAlpha=>true, :isPlanar=>false,
:colorSpaceName=>NSDeviceRGBColorSpace, :bytesPerRow=>0, :bitsPerPixel=>0)
NSGraphicsContext.setCurrentContext(NSGraphicsContext.graphicsContextWithBitmapImageRep(thumb))
NSGraphicsContext.currentContext.setImageInterpolation(NSImageInterpolationHigh)
img.drawInRect(thumbrect, :fromRect=>NSZeroRect, :operation=>NSCompositeCopy, :fraction=>1.0)
jpg = thumb.representationUsingType(NSJPEGFileType, :properties=>nil)
jpg.writeToFile("test_200.jpg", :atomically=>false)
NSGraphicsContext.restoreGraphicsState
end
지난번에 말씀드린 Holux M-241…
시간 간격(1, 5, 10, 15, 30, 60, 120초) 혹은 지정 거리(50, 100, 150, 300, 500, 1000m)별로 위치를 저장하고 총 13만개정도의 위치를 저장할 수 있으니 36시간 400초(13만초) – 180일 13시간 20분(15600000초) 정도의 시간, 혹은 6500Km – 130000Km의 거리를 저장할 수 있는 셈입니다.
BT747 프로그램을 사용하여 맥에서 로그 데이터를 받아 구글 어스에서 받는 것까지는 성공했지만 막상 전체 거리, 속도 등의 자료를 볼 수 없어서 이를 계산하는 프로그램을 짜 보았습니다. 프로그램은 XML의 형식을 가지는 KML 파일을 읽어들여 저장되어 있는 위도, 경도와 시간을 사용하여 이동거리 및 속도를 기록하고 이것을 기초로 간단한 그래프를 보여줍니다.
프로그램은 GPS 자료의 위도, 경도 차이를 기준으로 거리를 계산합니다. 위도, 경도를 계산하는 방법은 Haversine 방법과 Vincenty 방법 이 알려져 있는데 Vincenty 방법은 지구를 타원으로 측정하여 1m 정도까지 정확하게 계산할 수 있는 모양입니다. 집에서 직장까지의 거리를 Vincenty 방법과 Haversine 방법으로 계산한 결과는 각각 9.212Km, 9.206Km로 대략 60m 정도 차이가 나지만 어차피 GPS가 크게 정확하지 않으리라 생각하고 Haversine 방법으로 거리를 계산했습니다.
다음은 지난 금요일 자전거를 타고 출퇴근하면서 10초 간격으로 GPS를 기록한 것을 그래프로 나타낸 것입니다. 퇴근때는 GPS를 켜자마자 바로 달려서 초반 부분이 기록되어 있지 않습니다.
전체 프로그램의 소스입니다. 그래프는 rubyCocoa를 이용했기 때문에 Leopard이상의 OSX에서 실행해야 합니다. kml파일 목록을 인자로 실행시키면 기본적으로 GPS 기록이 1시간 이상 차이날 때마다 새로운 그래프를 만들어줍니다. KML 파일 이외의 형식도 마지막 부분의 소스를 조금만 수정하면 사용할 수 있도록 만들어져 있습니다.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 |
require 'rexml/document' require 'time' require 'osx/cocoa' # require 'rubygems' # require 'units' # great resources about distance calculations on web # http://williams.best.vwh.net/avform.htm # http://www.movable-type.co.uk/scripts/latlong.html # http://www.movable-type.co.uk/scripts/latlong-vincenty.html # http://ajax.suaccess.org/rubyisms-in-rails/converting-between-degrees-and-radians/ include Math include REXML SUMMARY_KO = { :dist=>"전체 거리 : %.2f Km", :time=>"시간 : %d분 %d초", :max_v=>"최고 속도 : %.2f Km/시", :avg_v=>"평균 속도 : %.2f Km/시" } SUMMARY_EN = { :dist=>"Total Distance : %.2f Km", :time=>"Time : %d:%d", :max_v=>"Max Velocity : %.2f Km/hr", :avg_v=>"Avg Velocity : %.2f Km/hr" } SUMMARY = SUMMARY_KO class Numeric def to_rad self*Math::PI/180 end # add_unit_conversions(:angle => { :radians => 1, :degrees => Math::PI/180 }) # add_unit_aliases(:angle => { :degrees => [:degree], :radians => [:radian] }) end def distVincenty(lat1, lon1, lat2, lon2) a, b = 6378137.0, 6356752.3142 f = 1/298.257223563; # WGS-84 ellipsiod l = (lon2-lon1).to_rad u1 = atan((1-f) * tan(lat1.to_rad)) u2 = atan((1-f) * tan(lat2.to_rad)) sinU1, cosU1 = sin(u1), cos(u1) sinU2, cosU2 = sin(u2), cos(u2) lambda, lambdaP = l, 2*Math::PI iterLimit = 19 while ((lambda-lambdaP).abs > 1e-12 and iterLimit>0) do sinLambda, cosLambda = sin(lambda), cos(lambda) sinSigma = sqrt((cosU2*sinLambda) ** 2 + (cosU1*sinU2-sinU1*cosU2*cosLambda) ** 2) return 0 if (sinSigma==0) # co-incident points cosSigma = sinU1*sinU2 + cosU1*cosU2*cosLambda sigma = atan2(sinSigma, cosSigma) sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma cosSqAlpha = 1 - sinAlpha ** 2 cos2SigmaM = cosSigma - 2*sinU1*sinU2/cosSqAlpha # if (isNaN(cos2SigmaM)) cos2SigmaM = 0 # equatorial line: cosSqAlpha=0 (§6) c = f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha)) lambdaP = lambda lambda = l + (1-c) * f * sinAlpha * \ (sigma + c*sinSigma*(cos2SigmaM+c*cosSigma*(-1+2*cos2SigmaM*cos2SigmaM))) iterLimit -= 1 end return nil if (iterLimit==0) # formula failed to converge uSq = cosSqAlpha * (a*a - b*b) / (b*b) biga = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq))) bigb = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq))) deltaSigma = bigb*sinSigma*(cos2SigmaM+bigb/4*(cosSigma*(-1+2*cos2SigmaM*cos2SigmaM) - \ bigb/6*cos2SigmaM*(-3+4*sinSigma*sinSigma)*(-3+4*cos2SigmaM*cos2SigmaM))) b*biga*(sigma-deltaSigma) end def distHaversine(lat1, lon1, lat2, lon2) # R = 6371 dLat = (lat2-lat1).to_rad dLon = (lon2-lon1).to_rad a = sin(dLat/2) * sin(dLat/2) + cos(lat1.to_rad) * cos(lat2.to_rad) * sin(dLon/2)**2 c = 2 * atan2(sqrt(a), sqrt(1-a)) 6371 * c * 1000; end def velocity(dist, t_int) (dist / 1000) / t_int * 3600 end def gps_calc(doc, path, loc_f, h_f, t_f, cut_interval = 60 * 5) result = [] rhash = { :data => [] } pre_lat, pre_lon, pre_h, pre_t = 0, 0, 0, 0 start_time, total_dist, total_points, time_int, max_v = nil, 0, 0, 0, 0 doc.elements.each(path) do |p| lat, lon = loc_f.call(p) h = h_f.call(p) t = t_f.call(p) start_time = t if start_time.nil? if pre_lat != 0 then time_int = t - pre_t if time_int <= cut_interval then dist = distHaversine(lat, lon, pre_lat, pre_lon) total_dist += dist total_points += 1 v = velocity(dist, time_int) max_v = v if v > max_v rhash[:data] << [t - start_time, dist, v] end end if time_int > cut_interval then avg_v = velocity(total_dist, pre_t - start_time) rhash[:start_time] = start_time rhash[:total_points]= total_points rhash[:total_dist] = total_dist / 1000 rhash[:time_int] = pre_t - start_time rhash[:avg_v] = avg_v rhash[:max_v] = max_v result << rhash rhash = { :data => [] } total_dist, total_points, max_v, time_int = 0, 0, 0, 0 pre_lat, pre_lon, start_time = 0, 0, nil else pre_lat, pre_lon, pre_h, pre_t = lat, lon, h, t end end if pre_lat != 0 then avg_v = velocity(total_dist, pre_t - start_time) rhash[:start_time] = start_time rhash[:total_points]= total_points rhash[:total_dist] = total_dist / 1000 rhash[:time_int] = pre_t - start_time rhash[:avg_v] = avg_v rhash[:max_v] = max_v result << rhash end result end def mark_x(gps_data, value, para, emphasis=false) position = value * para[:graph_width] / gps_data[:total_dist] + para[:margin] vstring = value.integer? ? value.to_s : "%.2f" % value label = OSX::NSString.alloc.initWithString(vstring) font_dict = emphasis ? para[:em_font_dict] : para[:font_dict] size = label.sizeWithAttributes(font_dict) label.drawAtPoint_withAttributes([position-size.width/2, para[:zero].y-size.height-para[:font_margin]], font_dict) OSX::NSRectFill([position, para[:zero].y-para[:font_margin], 1, para[:font_margin]]) end def mark_y(gps_data, value, para, emphasis=false) position = value * para[:graph_height] / (gps_data[:max_v] * 1) + para[:margin] vstring = value.integer? ? value.to_s : "%.2f" % value label = OSX::NSString.alloc.initWithString(vstring) font_dict = emphasis ? para[:em_font_dict] : para[:font_dict] size = label.sizeWithAttributes(font_dict) label.drawAtPoint_withAttributes([para[:margin]-size.width-para[:font_margin], position-size.height/2], font_dict) OSX::NSRectFill([para[:zero].x-para[:font_margin], position, para[:font_margin], 1]) end def make_graph(gps_data, para={}) default = { :width=>700, :height=>500, :mark_int=>60*10, :format=>OSX::NSPNGFileType, :margin=>50, :font_size=>12, :font_margin=>3 } dist_scales = [[1, 2, 5, 10, 20, 50, 100], [5, 10, 20, 50, 100, 200, 1000]] default.update(para) canvas = OSX::NSBitmapImageRep.alloc.initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(nil, default[:width], default[:height], 8, 4, true, false, OSX::NSDeviceRGBColorSpace, 0, 0) context = OSX::NSGraphicsContext.graphicsContextWithBitmapImageRep(canvas) OSX::NSGraphicsContext.setCurrentContext(context) # font white = OSX::NSColor.whiteColor white.set yellow = OSX::NSColor.yellowColor font = OSX::NSFont.fontWithName_size('Helvetica', default[:font_size]) font_dict = OSX::NSMutableDictionary.alloc.init font_dict.setObject_forKey(font, OSX::NSFontAttributeName) font_dict.setObject_forKey(white, OSX::NSForegroundColorAttributeName) default[:font_dict] = font_dict em_font = OSX::NSFont.boldSystemFontOfSize(default[:font_size]+2) em_font_dict = OSX::NSMutableDictionary.alloc.init em_font_dict.setObject_forKey(em_font, OSX::NSFontAttributeName) em_font_dict.setObject_forKey(OSX::NSColor.yellowColor, OSX::NSForegroundColorAttributeName) default[:em_font_dict] = em_font_dict # background gradient gradient = OSX::NSGradient.alloc.initWithStartingColor_endingColor(OSX::NSColor.blueColor, OSX::NSColor.blackColor) gradient.drawInRect_angle([0, 0, default[:width], default[:height]], 90) # lines default[:zero] = OSX::NSMakePoint(default[:margin], default[:margin]) default[:x_end] = OSX::NSMakePoint(default[:width]-default[:margin], default[:margin]) default[:y_end] = OSX::NSMakePoint(default[:margin], default[:height]-default[:margin]) path = OSX::NSBezierPath.bezierPath path.moveToPoint(default[:x_end]) path.lineToPoint(default[:zero]) path.lineToPoint(default[:y_end]) path.stroke # labels # dist_label = OSX::NSString.alloc.initWithString('Km') # dist_size = dist_label.sizeWithAttributes(font_dict) # dist_label.drawAtPoint_withAttributes([default[:x_end].x+default[:font_margin], default[:x_end].y-dist_size.height-default[:font_margin]], font_dict) # velo_label = OSX::NSString.alloc.initWithString('Km/Hr') # velo_size = velo_label.sizeWithAttributes(font_dict) # velo_label.drawAtPoint_withAttributes([default[:y_end].x-velo_size.width-default[:font_margin], default[:y_end].y+default[:font_margin]], font_dict) # default[:graph_width] = default[:width] - default[:margin] * 2 default[:graph_height] = default[:height] - default[:margin] * 2 # determine x scale units scale_index = 0 (dist_scales[1].size-1).downto(0) { |i| scale_index=i; break if gps_data[:total_dist] > dist_scales[1][i] } scale = dist_scales[0][scale_index] # draw x legends # mark_x(gps_data, gps_data[:total_dist], default, true) dist = scale while (dist < gps_data[:total_dist]) do mark_x(gps_data, dist, default) dist += scale end # mark_y(gps_data, gps_data[:max_v], default) velo = 10 while (velo < gps_data[:max_v]) do mark_y(gps_data, velo, default) velo += 10 end # draw graph graph = nil total = 0 gps_data[:data].each do |time_int, dist, velo| total += dist point_x = (total / 1000) * default[:graph_width] / gps_data[:total_dist] + default[:margin] point_y = velo * default[:graph_height] / (gps_data[:max_v] * 1) + default[:margin] if graph.nil? then graph = OSX::NSBezierPath.bezierPath graph.moveToPoint(OSX::NSMakePoint(point_x, point_y)) else graph.lineToPoint(OSX::NSMakePoint(point_x, point_y)) end if (default[:mark_int] != 0) and (time_int % default[:mark_int] == 0) then mark_label = OSX::NSString.alloc.initWithString("%d:%02d" % [time_int / 60, time_int % 60]) mark_size = mark_label.sizeWithAttributes(default[:font_dict]) mark_label.drawAtPoint_withAttributes([point_x-mark_size.width/2, point_y+default[:font_margin]], default[:font_dict]) mark = OSX::NSBezierPath.bezierPathWithOvalInRect([point_x-2, point_y-2, 4, 4]) yellow.set mark.stroke white.set end end graph.stroke # summary sum_y = default[:height] - default[:margin] / 2 dist_label = OSX::NSString.alloc.initWithString(SUMMARY[:dist] % gps_data[:total_dist]) dist_size = dist_label.sizeWithAttributes(default[:em_font_dict]) dist_label.drawAtPoint_withAttributes([default[:width]-default[:margin]/2-dist_size.width, sum_y-dist_size.height], default[:em_font_dict]) sum_y -= dist_size.height + default[:font_margin] time_label = OSX::NSString.alloc.initWithString(SUMMARY[:time] % [gps_data[:time_int]/60, gps_data[:time_int]%60]) time_size = time_label.sizeWithAttributes(default[:em_font_dict]) time_label.drawAtPoint_withAttributes([default[:width]-default[:margin]/2-time_size.width, sum_y-time_size.height], default[:em_font_dict]) sum_y -= time_size.height + default[:font_margin] maxv_label = OSX::NSString.alloc.initWithString(SUMMARY[:max_v] % gps_data[:max_v]) maxv_size = maxv_label.sizeWithAttributes(default[:em_font_dict]) maxv_label.drawAtPoint_withAttributes([default[:width]-default[:margin]/2-maxv_size.width, sum_y-maxv_size.height], default[:em_font_dict]) sum_y -= maxv_size.height + default[:font_margin] avgv_label = OSX::NSString.alloc.initWithString(SUMMARY[:avg_v] % gps_data[:avg_v]) avgv_size = avgv_label.sizeWithAttributes(default[:em_font_dict]) avgv_label.drawAtPoint_withAttributes([default[:width]-default[:margin]/2-avgv_size.width, sum_y-avgv_size.height], default[:em_font_dict]) canvas.representationUsingType_properties(default[:format], nil).rubyString end if ARGV.size > 0 then ARGV.each do |a| doc = Document.new(open(a)) loc_f = proc { |e| e.elements['Point'].elements['coordinates'].text.split(',').collect { |s| s.to_f}[0..2] } h_f = proc { |e| e.elements['Point'].elements['coordinates'].text.split(',').collect { |s| s.to_f}[2] } t_f = proc { |e| Time.parse(e.elements['TimeStamp'].elements['when'].text).localtime } r = gps_calc(doc, 'kml/Document/Folder/Folder/Placemark', loc_f, h_f, t_f, 60*60) r.each do |result| open("graph_#{result[:start_time].strftime('%Y%m%d%H%M')}.png", 'wb') { |f| f.write(make_graph(result))} end end end |
Cocoa의 기본 컨트롤에 TextView가 있습니다. API 목록의 함수 이름들을 둘러봤는데 매우 많은 기능을 가지고 있어 웬만한 워드프로세서도 만들수 있을 것 같습니다. TextView에서 다른 파일의 drop을 구현해 보고자 했는데 원래 커스텀 컨트롤에서 drag & drop을 구현하려면 여러가지 함수를 구현해야 합니다만, Interface Builder에서 붙여놓기만 해도 기본적인 drop을 받습니다.
파일을 받았는지의 여부와 어떤 파일을 받았는지를 알아내는 것이 문제가 되겠는데요… XCode의 문서에는 특별한 언급이 없는 것 같습니다. RubyCocoa에서 다음과 같이 서브클래싱으로 알아낼수 있었습니다.
class SubContent < OSX::NSTextView
# ns_overrides :performDragOperation_, :concludeDragOperation_
# def performDragOperation(sender)
# p 'PerformDragOperation', sender
# super_performDragOperation(sender)
# end
#
def concludeDragOperation(sender)
# p 'ConcludeDragOperation', sender
super_concludeDragOperation(sender)
range = selectedRanges[0].rangeValue
source = string[range.location...(range.location+range.length)].to_s
if Media.is_picture?(source) then
insertText('!file://'+source+'!')
elsif File.exist?(source) and File.ftype(source) == 'file' then
insertText('file://'+source)
end
Media.find_or_create(source)
setSelectedRange([0,0])
end
end
- 1행에서 SubContent란 클래스를 NSTextView에서 서브클래싱하고 있습니다.
- 2행은 원래 서브클래싱을 하려면 원래 클래스를 오버라이드하는 함수를 ns_overrides로 지정해 주어야 합니다만, 요즘 버전의 rubycocoa에서는 필요없게 되었습니다.
- drag은
draggingEntered:,draggingUpdated:,prepareForDragOperation:,performDragOperation:,concludeDragOperation:등의 함수로 구현하게 됩니다. 원래는 pasteBoard에서 파일정보를 얻어내야 하지만 TextView에서는 기본적으로 drag 받은 파일의 이름을 삽입하고 선택하게 되므로 concludeDragOperation만 오버라이드하였습니다. super_concludeDragOperation:을 먼저 호출했습니다. 단순히super함수를 호출하면 안 되고 super와 함수이름을_로 결합한 것으로 super 함수를 호출합니다. Ruby 클래스의 super와 구분하기 위해서 인것으로 생각됩니다.- 코드내에 위와 같은 클래스를 정의해 놓으면 Interface Builder에서 Custom View를 만들고 이것의 클래스를 앞서 정의한 SubContent와 같은 것으로 지정할 수 있습니다. (똑똑한 놈…)
- 단순히 Custom View만 만들면 윈도우의 크기가 변하거나 여러 행이 입력될때 view의 크기나 위치가 이상하게 이동하게 됩니다. ScrollView를 만들면 내부에 CustomView가 자동적으로 들어가 있어서 이것의 클래스를 지정할 수 있도록 되어 있습니다. (다시 똑똑한 놈…)
OSX에는 WebKit이란 브라우저가 사용하기 편하도록 포함되어 있습니다. 대략 apple에서 KDE에서 따온 것을 다시 오픈소스로 공개한것 같은데요, 지난번에 rubycocoa에서 간단하게 윈도우를 만든것에 이어서 오늘은 여기에 WebView란 컨트롤을 붙여보았습니다.
결과는…
Window가 화면에 표시되기전에 URL를 로드하도록 했고 NSScrollView를 추가하지 않았어도 자체적으로 스크롤바까지 만들어서 보여줘서 깜짝 놀랐습니다. 주소를 입력하는 곳이나 앞으로, 뒤로 등등의 기능은 없지만 링크도 클릭되고 플래시도 보여주는군요.
애플은 오픈소스를 참 잘 활용하고 있습니다. 서로 윈윈할 수 있으면 좋겠네요.
참고로 초간단 소스. 먼저의 소스에 4행이 추가되었습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
require 'osx/cocoa' include OSX require_framework 'Webkit' if $0 == __FILE__ then app = NSApplication.sharedApplication() frame = [200.0, 300.0, 250.0, 100.0] win = NSWindow.alloc.initWithContentRect_styleMask_backing_defer(frame, 15, 2, 0) win.setTitle 'HelloWorld' win.setLevel(3) # floating window browser = WebView.alloc.initWithFrame(win.frame) win.setContentView(browser) browser.mainFrame.loadRequest(NSURLRequest.requestWithURL(NSURL.URLWithString('http://www.daum.net'))) NSNotificationCenter.defaultCenter.addObserver_selector_name_object(app, 'terminate:', NSWindowWillCloseNotification, nil) win.display() win.orderFrontRegardless() ## but this one does app.run() end |
RubyCocoa는 ruby의 문법을 사용하면서 cocoa 어플리케이션을 만들어볼 수 있어서 매력적입니다. 보통 Xcode와 Interface Builder를 사용하여 NIB파일을 만들고 사용합니다만 다음과 같은 코드로 간단히 기본적인 윈도우를 만들어 볼 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
require 'osx/cocoa' include OSX if $0 == __FILE__ then app = NSApplication.sharedApplication() frame = [200.0, 300.0, 250.0, 100.0] win = NSWindow.alloc.initWithContentRect_styleMask_backing_defer(frame, 15, 2, 0) win.setTitle 'HelloWorld' win.setLevel(3) # floating window NSNotificationCenter.defaultCenter.addObserver_selector_name_object(app, 'terminate:', NSWindowWillCloseNotification, nil) win.display() win.orderFrontRegardless() ## but this one does app.run() end |
Leopard내의 RubyCocoa Example 디렉토리의 HelloWorld.rb 프로그램의 간략버전입니다. 원 버전에서는 Notification을 사용하지 않고 버튼을 눌렀을때 app의 stop이 호출되도록 되어있습니다.
윈도우를 만들어 띄우는것 까지는 비교적 간단한데 끝내는 버튼을 눌렀을때 프로그램이 끝나도록 하는 방법을 찾느라 고생했습니다. 인자를 잘 못 주면 segmentation fault가 나는데 요리조리 바꾸어 조합하면서 시험해보니 NSApplication 객체에 terminate 함수가 바로 호출되도록 하는 것이 제대로 동작하는듯 합니다. Notification의 사용에 대해서는 좀 더 알아봐야 되겠습니다.
